summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/calendar-sync.el91
-rw-r--r--modules/dashboard-config.el24
-rw-r--r--modules/keyboard-compat.el170
-rw-r--r--modules/terminal-compat.el54
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