summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-02 08:34:36 -0600
committerCraig Jennings <c@cjennings.net>2026-02-02 08:34:36 -0600
commit089a4313660cb8af1eca3829ffbdbae70f72333a (patch)
treec85f6a5f251b7c2a1de0088576c4c19c56d07305
parentba27bf84935e8820b9f9bb946d284254275e3216 (diff)
feat(keyboard): add GUI key translation for M-S- bindingsHEADmain
Rename terminal-compat.el to keyboard-compat.el and add GUI support. Problem: M-S-o and other Meta+Shift bindings didn't work in GUI mode. GUI Emacs receives M-O (uppercase) but bindings use M-S-o syntax. Terminal can't use M-O due to arrow key escape sequence conflicts. Solution: Use key-translation-map in GUI mode to translate M-O -> M-S-o for all 18 Meta+Shift keybindings. Terminal fixes unchanged. Also fix two test issues: - Remove expected-fail from expand-weekly test (timezone fix resolved it) - Add helpful install messages to dependency-checking tests
-rw-r--r--init.el2
-rw-r--r--modules/keyboard-compat.el170
-rw-r--r--modules/terminal-compat.el54
-rw-r--r--tests/test-calendar-sync--expand-weekly.el4
-rw-r--r--tests/test-flycheck-languagetool-setup.el12
5 files changed, 180 insertions, 62 deletions
diff --git a/init.el b/init.el
index 9bb1f7fb..9757dc48 100644
--- a/init.el
+++ b/init.el
@@ -22,7 +22,7 @@
(require 'config-utilities) ;; enable for extra Emacs config debug helpers
(require 'user-constants) ;; paths for files referenced in this config
(require 'host-environment) ;; convenience functions re: host environment
-(require 'terminal-compat) ;; terminal/mosh compatibility fixes
+(require 'keyboard-compat) ;; terminal/GUI keyboard compatibility
(require 'system-defaults) ;; native comp; log; unicode, backup, exec path
(require 'keybindings) ;; system-wide keybindings and keybinding discovery
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
diff --git a/tests/test-calendar-sync--expand-weekly.el b/tests/test-calendar-sync--expand-weekly.el
index d7b0eddc..fe333c98 100644
--- a/tests/test-calendar-sync--expand-weekly.el
+++ b/tests/test-calendar-sync--expand-weekly.el
@@ -24,9 +24,7 @@
;;; Normal Cases
(ert-deftest test-calendar-sync--expand-weekly-normal-saturday-returns-occurrences ()
- "Test expanding weekly event on Saturday (GTFO use case).
-Known issue: Timezone calculation may cause off-by-one day error."
- :expected-result :failed
+ "Test expanding weekly event on Saturday (GTFO use case)."
(test-calendar-sync--expand-weekly-setup)
(unwind-protect
(let* ((start-date (test-calendar-sync-time-days-from-now 1 10 30))
diff --git a/tests/test-flycheck-languagetool-setup.el b/tests/test-flycheck-languagetool-setup.el
index a719e822..aa71d4a7 100644
--- a/tests/test-flycheck-languagetool-setup.el
+++ b/tests/test-flycheck-languagetool-setup.el
@@ -29,12 +29,16 @@
(should (file-executable-p wrapper-path))))
(ert-deftest test-flycheck-languagetool-setup-normal-languagetool-installed ()
- "Test that languagetool command is available in PATH."
- (should (executable-find "languagetool")))
+ "Test that languagetool command is available in PATH.
+The test failure serves as a reminder to install the dependency."
+ (should (or (executable-find "languagetool")
+ (error "LanguageTool not installed. Install with: sudo pacman -S languagetool"))))
(ert-deftest test-flycheck-languagetool-setup-normal-python3-available ()
- "Test that python3 is available for wrapper script."
- (should (executable-find "python3")))
+ "Test that python3 is available for wrapper script.
+The test failure serves as a reminder to install the dependency."
+ (should (or (executable-find "python3")
+ (error "python3 not installed. Install with: sudo pacman -S python"))))
;; ----------------------------- Boundary Cases --------------------------------