diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/calendar-sync.el | 91 | ||||
| -rw-r--r-- | modules/dashboard-config.el | 24 | ||||
| -rw-r--r-- | modules/keyboard-compat.el | 170 | ||||
| -rw-r--r-- | modules/terminal-compat.el | 54 |
4 files changed, 273 insertions, 66 deletions
diff --git a/modules/calendar-sync.el b/modules/calendar-sync.el index fa524f6a..582c482d 100644 --- a/modules/calendar-sync.el +++ b/modules/calendar-sync.el @@ -320,6 +320,25 @@ Returns nil if property not found." (setq start (match-end 0))) value))) +(defun calendar-sync--get-property-line (event property) + "Extract full PROPERTY line from EVENT string, including parameters. +Returns the complete line like 'DTSTART;TZID=Europe/Lisbon:20260202T190000'. +Returns nil if property not found." + (when (string-match (format "^\\(%s[^\n]*\\)$" (regexp-quote property)) event) + (match-string 1 event))) + +(defun calendar-sync--extract-tzid (property-line) + "Extract TZID parameter value from PROPERTY-LINE. +PROPERTY-LINE is like 'DTSTART;TZID=Europe/Lisbon:20260202T190000'. +Returns timezone string like 'Europe/Lisbon', or nil if no TZID. +Returns nil for malformed lines (missing colon separator)." + (when (and property-line + (stringp property-line) + ;; Must have colon (property:value format) + (string-match-p ":" property-line) + (string-match ";TZID=\\([^;:]+\\)" property-line)) + (match-string 1 property-line))) + (defun calendar-sync--convert-utc-to-local (year month day hour minute second) "Convert UTC datetime to local time. Returns list (year month day hour minute) in local timezone." @@ -331,10 +350,42 @@ Returns list (year month day hour minute) in local timezone." (nth 2 local-time) ; hour (nth 1 local-time)))) ; minute -(defun calendar-sync--parse-timestamp (timestamp-str) +(defun calendar-sync--convert-tz-to-local (year month day hour minute source-tz) + "Convert datetime from SOURCE-TZ timezone to local time. +SOURCE-TZ is a timezone name like 'Europe/Lisbon' or 'Asia/Yerevan'. +Returns list (year month day hour minute) in local timezone, or nil on error. + +Uses the system `date` command for reliable timezone conversion." + (when (and source-tz (not (string-empty-p source-tz))) + (condition-case err + (let* ((date-input (format "%04d-%02d-%02d %02d:%02d" + year month day hour minute)) + ;; Use date command: convert from source-tz to local + ;; TZ= sets output timezone (local), TZ=\"...\" in -d sets input timezone + (cmd (format "date -d 'TZ=\"%s\" %s' '+%%Y %%m %%d %%H %%M' 2>/dev/null" + source-tz date-input)) + (result (string-trim (shell-command-to-string cmd))) + (parts (split-string result " "))) + (if (= 5 (length parts)) + (list (string-to-number (nth 0 parts)) + (string-to-number (nth 1 parts)) + (string-to-number (nth 2 parts)) + (string-to-number (nth 3 parts)) + (string-to-number (nth 4 parts))) + ;; date command failed (invalid timezone, etc.) + (cj/log-silently "calendar-sync: Failed to convert timezone %s: %s" + source-tz result) + nil)) + (error + (cj/log-silently "calendar-sync: Error converting timezone %s: %s" + source-tz (error-message-string err)) + nil)))) + +(defun calendar-sync--parse-timestamp (timestamp-str &optional tzid) "Parse iCal timestamp string TIMESTAMP-STR. Returns (year month day hour minute) or (year month day) for all-day events. Converts UTC times (ending in Z) to local time. +If TZID is provided (e.g., 'Europe/Lisbon'), converts from that timezone to local. Returns nil if parsing fails." (cond ;; DateTime format: 20251116T140000Z or 20251116T140000 @@ -346,9 +397,18 @@ Returns nil if parsing fails." (minute (string-to-number (match-string 5 timestamp-str))) (second (string-to-number (match-string 6 timestamp-str))) (is-utc (match-string 7 timestamp-str))) - (if is-utc - (calendar-sync--convert-utc-to-local year month day hour minute second) - (list year month day hour minute)))) + (cond + ;; UTC timestamp (Z suffix) - convert from UTC + (is-utc + (calendar-sync--convert-utc-to-local year month day hour minute second)) + ;; TZID provided - convert from that timezone + (tzid + (or (calendar-sync--convert-tz-to-local year month day hour minute tzid) + ;; Fallback to raw time if conversion fails + (list year month day hour minute))) + ;; No timezone info - assume local time + (t + (list year month day hour minute))))) ;; Date format: 20251116 ((string-match "\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)" timestamp-str) (list (string-to-number (match-string 1 timestamp-str)) @@ -582,17 +642,24 @@ Returns list of event plists, or nil if not a recurring event." "Parse single VEVENT string EVENT-STR into plist. Returns plist with :summary :description :location :start :end. Returns nil if event lacks required fields (DTSTART, SUMMARY). -Skips events with RECURRENCE-ID (individual instances of recurring events)." +Skips events with RECURRENCE-ID (individual instances of recurring events). +Handles TZID-qualified timestamps by converting to local time." ;; Skip individual instances of recurring events (they're handled by RRULE expansion) (unless (calendar-sync--get-property event-str "RECURRENCE-ID") - (let ((summary (calendar-sync--get-property event-str "SUMMARY")) - (description (calendar-sync--get-property event-str "DESCRIPTION")) - (location (calendar-sync--get-property event-str "LOCATION")) - (dtstart (calendar-sync--get-property event-str "DTSTART")) - (dtend (calendar-sync--get-property event-str "DTEND"))) + (let* ((summary (calendar-sync--get-property event-str "SUMMARY")) + (description (calendar-sync--get-property event-str "DESCRIPTION")) + (location (calendar-sync--get-property event-str "LOCATION")) + ;; Get raw property values + (dtstart (calendar-sync--get-property event-str "DTSTART")) + (dtend (calendar-sync--get-property event-str "DTEND")) + ;; Extract TZID from property lines (if present) + (dtstart-line (calendar-sync--get-property-line event-str "DTSTART")) + (dtend-line (calendar-sync--get-property-line event-str "DTEND")) + (start-tzid (calendar-sync--extract-tzid dtstart-line)) + (end-tzid (calendar-sync--extract-tzid dtend-line))) (when (and summary dtstart) - (let ((start-parsed (calendar-sync--parse-timestamp dtstart)) - (end-parsed (and dtend (calendar-sync--parse-timestamp dtend)))) + (let ((start-parsed (calendar-sync--parse-timestamp dtstart start-tzid)) + (end-parsed (and dtend (calendar-sync--parse-timestamp dtend end-tzid)))) (when start-parsed (list :summary summary :description description diff --git a/modules/dashboard-config.el b/modules/dashboard-config.el index 918acdf2..3333d96d 100644 --- a/modules/dashboard-config.el +++ b/modules/dashboard-config.el @@ -47,6 +47,15 @@ (t (format dashboard-bookmarks-item-format filename path-shorten))) el))) +;; ------------------------- Banner Title Centering Fix ------------------------ +;; The default centering can be off due to font width calculations. +;; This override allows manual adjustment via dashboard-banner-title-offset. + +(defvar dashboard-banner-title-offset 5 + "Offset to adjust banner title centering. +Positive values shift left, negative values shift right. +Adjust this if the title doesn't appear centered under the banner image.") + ;; ----------------------------- Display Dashboard ----------------------------- ;; convenience function to redisplay dashboard and kill all other windows @@ -182,5 +191,20 @@ (define-key dashboard-mode-map (kbd "t") (lambda () (interactive) (vterm))) (define-key dashboard-mode-map (kbd "d") (lambda () (interactive) (dirvish user-home-dir)))) +;; Override banner title centering (must be after dashboard-widgets loads) +(with-eval-after-load 'dashboard-widgets + (defun dashboard-insert-banner-title () + "Insert `dashboard-banner-logo-title' with adjustable centering offset." + (when dashboard-banner-logo-title + (let* ((title dashboard-banner-logo-title) + (start (point))) + (insert (propertize title 'face 'dashboard-banner-logo-title)) + (let* ((end (point)) + (width (string-width title)) + (adjusted-center (+ (/ (float width) 2) dashboard-banner-title-offset)) + (prefix (propertize " " 'display `(space . (:align-to (- center ,adjusted-center)))))) + (add-text-properties start end `(line-prefix ,prefix indent-prefix ,prefix)))) + (insert "\n")))) + (provide 'dashboard-config) ;;; dashboard-config.el ends here. diff --git a/modules/keyboard-compat.el b/modules/keyboard-compat.el new file mode 100644 index 00000000..9b277ba8 --- /dev/null +++ b/modules/keyboard-compat.el @@ -0,0 +1,170 @@ +;;; keyboard-compat.el --- Keyboard compatibility for terminal and GUI -*- lexical-binding: t; coding: utf-8; -*- +;; author: Craig Jennings <c@cjennings.net> + +;;; Commentary: + +;; This module fixes keyboard input differences between terminal and GUI Emacs. +;; +;; THE PROBLEM: Meta+Shift keybindings behave differently in terminal vs GUI +;; ========================================================================= +;; +;; In Emacs, there are two ways to express "Meta + Shift + o": +;; +;; 1. M-O (Meta + uppercase O) - key code 134217807 +;; 2. M-S-o (Meta + explicit Shift modifier + lowercase o) - key code 167772271 +;; +;; These are NOT the same key in Emacs! +;; +;; GUI Emacs behavior: +;; When you press Meta+Shift+o on your keyboard, GUI Emacs receives M-O +;; (uppercase O). It does NOT receive M-S-o. This is because the keyboard +;; sends Shift+o as uppercase 'O', not as a Shift modifier plus lowercase 'o'. +;; +;; Terminal Emacs behavior: +;; Terminals send escape sequences for special keys. Arrow keys send: +;; - Up: ESC O A +;; - Down: ESC O B +;; - Right: ESC O C +;; - Left: ESC O D +;; +;; The problem: ESC O is interpreted as M-O by Emacs! So if you bind M-O +;; to a function, pressing the up arrow sends "ESC O A", Emacs sees "M-O" +;; and triggers your function instead of moving up. Arrow keys break. +;; +;; THE SOLUTION: Different handling for each display type +;; ====================================================== +;; +;; For terminal mode (handled by cj/keyboard-compat-terminal-setup): +;; - Use input-decode-map to translate arrow escape sequences BEFORE +;; any keybinding lookup. ESC O A becomes [up], not M-O followed by A. +;; - Keybindings use M-S-o syntax (some terminals support explicit Shift) +;; - Disable graphical icons that show as unicode artifacts +;; +;; For GUI mode (handled by cj/keyboard-compat-gui-setup): +;; - Use key-translation-map to translate M-O to M-S-o BEFORE lookup +;; - This way, pressing Meta+Shift+o (which sends M-O) gets translated +;; to M-S-o, matching the keybinding definitions +;; - All 18 Meta+Shift keybindings work correctly +;; +;; WHY NOT JUST USE M-O FOR KEYBINDINGS? +;; ===================================== +;; +;; We could bind to M-O directly, but: +;; 1. Terminal arrow keys would break (ESC O prefix conflict) +;; 2. We'd need to maintain two sets of bindings (M-O for GUI, something +;; else for terminal) +;; +;; By using M-S-o syntax everywhere and translating M-O -> M-S-o in GUI mode, +;; we have one consistent set of keybindings that work everywhere. +;; +;; KEYBINDINGS AFFECTED: +;; ==================== +;; +;; The following M-S- keybindings are translated from M-uppercase in GUI: +;; +;; M-O -> M-S-o cj/kill-other-window (undead-buffers.el) +;; M-M -> M-S-m cj/kill-all-other-buffers-and-windows (undead-buffers.el) +;; M-Y -> M-S-y yank-media (keybindings.el) +;; M-F -> M-S-f fontaine-set-preset (font-config.el) +;; M-W -> M-S-w wttrin (weather-config.el) +;; M-E -> M-S-e eww (eww-config.el) +;; M-L -> M-S-l cj/switch-themes (ui-theme.el) +;; M-R -> M-S-r cj/elfeed-open (elfeed-config.el) +;; M-V -> M-S-v cj/split-and-follow-right (ui-navigation.el) +;; M-H -> M-S-h cj/split-and-follow-below (ui-navigation.el) +;; M-T -> M-S-t toggle-window-split (ui-navigation.el) +;; M-S -> M-S-s window-swap-states (ui-navigation.el) +;; M-Z -> M-S-z cj/undo-kill-buffer (ui-navigation.el) +;; M-U -> M-S-u winner-undo (ui-navigation.el) +;; M-D -> M-S-d dwim-shell-commands-menu (dwim-shell-config.el) +;; M-I -> M-S-i edit-indirect-region (text-config.el) +;; M-C -> M-S-c time-zones (chrono-tools.el) +;; M-B -> M-S-b calibredb (calibredb-epub-config.el) +;; M-K -> M-S-k show-kill-ring (show-kill-ring.el) + +;;; Code: + +(require 'host-environment) + +;; ============================================================================= +;; Terminal-specific fixes +;; ============================================================================= + +(defun cj/keyboard-compat-terminal-setup () + "Set up keyboard compatibility for terminal/console mode. +This runs after init to override any package settings." + (when (env-terminal-p) + ;; Fix arrow key escape sequences for various terminal types + ;; These must be decoded BEFORE keybinding lookup to prevent + ;; M-O prefix from intercepting arrow keys + (define-key input-decode-map "\e[A" [up]) + (define-key input-decode-map "\e[B" [down]) + (define-key input-decode-map "\e[C" [right]) + (define-key input-decode-map "\e[D" [left]) + + ;; Application mode arrows (sent by some terminals like xterm) + (define-key input-decode-map "\eOA" [up]) + (define-key input-decode-map "\eOB" [down]) + (define-key input-decode-map "\eOC" [right]) + (define-key input-decode-map "\eOD" [left]))) + +;; Run after init completes to override any package settings +(add-hook 'emacs-startup-hook #'cj/keyboard-compat-terminal-setup) + +;; Icon disabling only in terminal mode (prevents unicode artifacts) +(when (env-terminal-p) + ;; Disable nerd-icons display (shows as \uXXXX artifacts) + (with-eval-after-load 'nerd-icons + (defun nerd-icons-icon-for-file (&rest _) "") + (defun nerd-icons-icon-for-dir (&rest _) "") + (defun nerd-icons-icon-for-mode (&rest _) "") + (defun nerd-icons-icon-for-buffer (&rest _) "")) + + ;; Disable dashboard icons + (with-eval-after-load 'dashboard + (setq dashboard-display-icons-p nil) + (setq dashboard-set-file-icons nil) + (setq dashboard-set-heading-icons nil)) + + ;; Disable all-the-icons + (with-eval-after-load 'all-the-icons + (defun all-the-icons-icon-for-file (&rest _) "") + (defun all-the-icons-icon-for-dir (&rest _) "") + (defun all-the-icons-icon-for-mode (&rest _) ""))) + +;; ============================================================================= +;; GUI-specific fixes +;; ============================================================================= + +(defun cj/keyboard-compat-gui-setup () + "Set up keyboard compatibility for GUI mode. +Translates M-uppercase keys to M-S-lowercase so that pressing +Meta+Shift+letter triggers M-S-letter keybindings." + (when (env-gui-p) + ;; Translate M-O (what keyboard sends) to M-S-o (what keybindings use) + ;; key-translation-map runs before keybinding lookup + (define-key key-translation-map (kbd "M-O") (kbd "M-S-o")) + (define-key key-translation-map (kbd "M-M") (kbd "M-S-m")) + (define-key key-translation-map (kbd "M-Y") (kbd "M-S-y")) + (define-key key-translation-map (kbd "M-F") (kbd "M-S-f")) + (define-key key-translation-map (kbd "M-W") (kbd "M-S-w")) + (define-key key-translation-map (kbd "M-E") (kbd "M-S-e")) + (define-key key-translation-map (kbd "M-L") (kbd "M-S-l")) + (define-key key-translation-map (kbd "M-R") (kbd "M-S-r")) + (define-key key-translation-map (kbd "M-V") (kbd "M-S-v")) + (define-key key-translation-map (kbd "M-H") (kbd "M-S-h")) + (define-key key-translation-map (kbd "M-T") (kbd "M-S-t")) + (define-key key-translation-map (kbd "M-S") (kbd "M-S-s")) + (define-key key-translation-map (kbd "M-Z") (kbd "M-S-z")) + (define-key key-translation-map (kbd "M-U") (kbd "M-S-u")) + (define-key key-translation-map (kbd "M-D") (kbd "M-S-d")) + (define-key key-translation-map (kbd "M-I") (kbd "M-S-i")) + (define-key key-translation-map (kbd "M-C") (kbd "M-S-c")) + (define-key key-translation-map (kbd "M-B") (kbd "M-S-b")) + (define-key key-translation-map (kbd "M-K") (kbd "M-S-k")))) + +;; Run early - key-translation-map should be set up before keybindings +(add-hook 'emacs-startup-hook #'cj/keyboard-compat-gui-setup) + +(provide 'keyboard-compat) +;;; keyboard-compat.el ends here diff --git a/modules/terminal-compat.el b/modules/terminal-compat.el deleted file mode 100644 index f959646d..00000000 --- a/modules/terminal-compat.el +++ /dev/null @@ -1,54 +0,0 @@ -;;; terminal-compat.el --- Terminal compatibility fixes -*- lexical-binding: t; coding: utf-8; -*- -;; author: Craig Jennings <c@cjennings.net> - -;;; Commentary: - -;; Fixes for running Emacs in terminal/console mode, especially over mosh. -;; - Arrow key escape sequence handling -;; - Disable graphical icons that show as unicode artifacts - -;;; Code: - -(require 'host-environment) - -(defun cj/terminal-compat-setup () - "Set up terminal compatibility after init completes." - (when (env-terminal-p) - ;; Fix arrow key escape sequences for various terminal types - (define-key input-decode-map "\e[A" [up]) - (define-key input-decode-map "\e[B" [down]) - (define-key input-decode-map "\e[C" [right]) - (define-key input-decode-map "\e[D" [left]) - - ;; Application mode arrows (sent by some terminals) - (define-key input-decode-map "\eOA" [up]) - (define-key input-decode-map "\eOB" [down]) - (define-key input-decode-map "\eOC" [right]) - (define-key input-decode-map "\eOD" [left]))) - -;; Run after init completes to override any package settings -(add-hook 'emacs-startup-hook #'cj/terminal-compat-setup) - -;; Icon disabling only in terminal mode -(when (env-terminal-p) - ;; Disable nerd-icons display (shows as \uXXXX artifacts) - (with-eval-after-load 'nerd-icons - (defun nerd-icons-icon-for-file (&rest _) "") - (defun nerd-icons-icon-for-dir (&rest _) "") - (defun nerd-icons-icon-for-mode (&rest _) "") - (defun nerd-icons-icon-for-buffer (&rest _) "")) - - ;; Disable dashboard icons - (with-eval-after-load 'dashboard - (setq dashboard-display-icons-p nil) - (setq dashboard-set-file-icons nil) - (setq dashboard-set-heading-icons nil)) - - ;; Disable all-the-icons - (with-eval-after-load 'all-the-icons - (defun all-the-icons-icon-for-file (&rest _) "") - (defun all-the-icons-icon-for-dir (&rest _) "") - (defun all-the-icons-icon-for-mode (&rest _) ""))) - -(provide 'terminal-compat) -;;; terminal-compat.el ends here |
