aboutsummaryrefslogtreecommitdiff
path: root/modules/keyboard-compat.el
blob: 172f96c7b8c91143332d3adab6013a51c05ff8a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
;;; keyboard-compat.el --- Keyboard compatibility for terminal and GUI -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>

;;; Commentary:
;;
;; Layer: 1 (Foundation).
;; Category: F/S.
;; Load shape: eager.
;; Eager reason: normalizes terminal/GUI key input before custom bindings matter.
;; Top-level side effects: adds cj/keyboard-compat-terminal-setup to startup.
;; Runtime requires: host-environment.
;; Direct test load: yes.
;;
;; Normalizes Meta+Shift bindings across GUI and terminal frames. GUI frames
;; translate M-uppercase events to explicit M-S-lowercase keys; terminal frames
;; decode arrow escape sequences before key lookup so ESC O prefixes do not trip
;; M-S bindings.
;;
;; Also provides terminal-specific display fallbacks, such as hiding icon glyphs
;; that render poorly outside GUI frames.

;;; 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-rendering functions return blank on terminal frames so unicode
;; artifacts don't show up. The check runs per call against the selected
;; frame, so the same daemon serves real icons to GUI clients and blanks to
;; terminal clients. Earlier this lived in a top-level (when (env-terminal-p)
;; ...) block that redefined the icon functions at module-load time, which
;; broke under daemon startup: no frame exists yet, display-graphic-p returns
;; nil, env-terminal-p returns t, and the stubs install permanently. GUI
;; clients connecting later saw empty icons everywhere.

(defun cj/--icon-blank-in-terminal (orig &rest args)
  "Return empty string on a terminal frame, otherwise call ORIG with ARGS."
  (if (display-graphic-p) (apply orig args) ""))

(with-eval-after-load 'nerd-icons
  (dolist (fn '(nerd-icons-icon-for-file
                nerd-icons-icon-for-dir
                nerd-icons-icon-for-mode
                nerd-icons-icon-for-buffer))
    (advice-add fn :around #'cj/--icon-blank-in-terminal)))

(with-eval-after-load 'all-the-icons
  (dolist (fn '(all-the-icons-icon-for-file
                all-the-icons-icon-for-dir
                all-the-icons-icon-for-mode))
    (advice-add fn :around #'cj/--icon-blank-in-terminal)))

;; =============================================================================
;; 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-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"))))

;; In daemon mode, no frame exists at startup so env-gui-p returns nil.
;; Use server-after-make-frame-hook to set up translations when the first
;; GUI client connects. In non-daemon mode, run at startup as before.
;;
;; `add-hook' is idempotent for named functions (re-adding the same
;; symbol is a no-op), and `cj/keyboard-compat-gui-setup' itself only
;; calls `define-key' -- each key has one binding regardless of how many
;; times the function fires -- so this block is safe under repeated
;; module loads.
(if (daemonp)
    (add-hook 'server-after-make-frame-hook #'cj/keyboard-compat-gui-setup)
  (add-hook 'emacs-startup-hook #'cj/keyboard-compat-gui-setup))

(provide 'keyboard-compat)
;;; keyboard-compat.el ends here