unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
From: Cecilio Pardo <cpardo@imayhem.com>
To: Stefan Monnier <monnier@iro.umontreal.ca>,
	Eli Zaretskii <eliz@gnu.org>, Po Lu <luangruo@yahoo.com>
Cc: 74423@debbugs.gnu.org
Subject: bug#74423: Low level key events
Date: Mon, 2 Dec 2024 17:54:03 +0100	[thread overview]
Message-ID: <b0b914a0-b454-41b0-aa3d-d9e243c210f5@imayhem.com> (raw)
In-Reply-To: <jwvplmqtuh1.fsf-monnier+emacs@gnu.org>

[-- Attachment #1: Type: text/plain, Size: 1643 bytes --]

Here is a new version of the patch. Sorry it took so long. It
addresses most of the notes I got:

Notes by Po Lu:
   - I think now the associated modifier for a key is computed
     properly, with or without XKB for plain X, and for gtk.
   - 'ralt' not generated for ISO_Level3_Shift. This will have to stay
     like this. What we know about the keys if what the system lets us.
   - Don't call x_any_window_to_frame if already called for the event.

Notes by Eli:
   - Change testing for Venable_low_level_key_events. This variable now
     is more complex.

Notes by Stefan:
   - Created and used event-* functions to access event.
   - Only one event with both pieces of information.
   - Selection of keys. Now the variable enable-low-level-key-events
     can be used to select exactly which keys we want to process.
   - About creating lisp object in handle_one_xevent, it is done in
     other events too.

The lisp interface is as follows:
   - A list of variables with keysym values is initialized (xk-*), so the
     user can select keys, not just modifiers.
   - Users can bind globally commands or functions with llk-bind to taps.
   - To use a tapped key as a new modifier, this works:
     (llk-bind 'tap 'xk-shift-r (lambda ()
                (message "H-...")
                (setq unread-command-events
                   (append (event-apply-hyper-modifier nil) nil))))
   - Added the function describe-low-level-key to see what
     keysym/modifier it triggers.


This is implemented for X, pgtkw and MS-Windows.
The pgtj implementation detects that a key is a modifier to filter it, 
but can't decide which one it its.



[-- Attachment #2: 0001-Send-event-for-key-presses-and-key-releases.patch --]
[-- Type: text/plain, Size: 36568 bytes --]

From 863068f4e1e187ba33382c0debb8caee5bbe5cbe Mon Sep 17 00:00:00 2001
From: Cecilio Pardo <cpardo@imayhem.com>
Date: Mon, 2 Dec 2024 17:30:42 +0100
Subject: [PATCH] Send event for key presses and key releases.

Detect double/tripe taps.

* src/gtkutil.c (xg_create_frame_widgets): Modified to handle key
release events.
(xg_maybe_send_low_level_key_event): New function that sends key
events.
(xg_widget_key_press_event_cb): Modified to send low level key events.
(xg_widget_key_release_event_cb):New function to send low level key
events on key release.
* src/keyboard.c (kbd_buffer_get_event): Modified to handle low level
keyboard events.
(make_lispy_event): Modified to handle low level keyboard events.
(kbd_low_level_key_is_enabled): New function to decice if a particular
key should generate events, looking at configuration,
(syms_of_keyboard): Added symbols and varialbles.
(keys_of_keyboard): Modified to map low level key events to
'special-event-map'.
* src/keyboard.h: Modified with function prototype.
* src/pgtkterm.c (pgtk_maybe_send_low_level_key_event): New function
that sends key events.
(key_press_event): Modified to handle low level keyboard events.
(key_release_event): Modified to handle low level keyboard events.
* src/termhooks.h: Modified to define constant.
* src/w32fns.c (w32_wnd_proc): Modified to generate low level key
events.
* src/w32term.c (w32_read_socket): Modified to generate low level key
events.
* src/w32term.h (WM_EMACS_IME_STATUS): Modified to define constants.
* src/xterm.c (x_get_modifier_for_keycode): New function to decide
which (if any) modifier corresponds to a given key.
(x_maybe_send_low_level_key_event): New function that sends key
events.
(handle_one_xevent): Modified to handle low level key events.
(syms_of_xterm): Modified to definde symbol.
* src/xterm.h: Modified with function prototype
* lisp/low-level-key.el (New file).
(llk-bindings): User bindings for low level key events.
(llk-tap-count): User option.
(llk-tap-timeout): User option.
(llk-tap-keys): User option.
(llk-keysyms): List of available keysyms.
(define-xk): Macro for defining keysyms.
(llk-define-keysyms): Build llk-keysyms.
(llk-init): Function to initialize low level key handling.
(event-is-key-press): Get field from event.
(event-keysym): Get field from event.
(event-modifier): Get field from event.
(event-time): Get field from event.
(llk-bind): Function to create a binding.
(llk-events): Event history for tap detection.
(llk-detect-n-tap): Function to detect taps.
(describe-low-level-key): Command to get information about a key.
(llk-show-event-description): Show help buffer with information
about an event.
(llk-handle): Handler for key events.
---
 lisp/low-level-key.el | 335 ++++++++++++++++++++++++++++++++++++++++++
 src/gtkutil.c         |  81 ++++++++++
 src/keyboard.c        |  61 ++++++++
 src/keyboard.h        |   1 +
 src/pgtkterm.c        |  55 +++++++
 src/termhooks.h       |   1 +
 src/w32fns.c          |  11 ++
 src/w32term.c         |  49 ++++++
 src/w32term.h         |   3 +-
 src/xterm.c           | 142 ++++++++++++++++++
 src/xterm.h           |   2 +
 11 files changed, 740 insertions(+), 1 deletion(-)
 create mode 100644 lisp/low-level-key.el

diff --git a/lisp/low-level-key.el b/lisp/low-level-key.el
new file mode 100644
index 00000000000..85cdaaaf0f4
--- /dev/null
+++ b/lisp/low-level-key.el
@@ -0,0 +1,335 @@
+;;; -*- lexical-binding: t -*-
+
+;; The physical-key event is like this:
+;; (physical-key IS-KEY-PRESS KEY MODIFIER TIME FRAME)
+;; IS-KEY-PRESS is t if the key has been pressed, nil if it has been released.
+;; KEY is the keysym number.
+;; MODIFIER is the modifier associated with this key. It is nil if the key is
+;; not a modifier. It can be one of the following symbols: shift, control, meta,
+;; super, hyper, alt. It can also be t if the key is a modifier but it can't be
+;; identified.
+;; TIME is the timestamp in milliseconds of the event.
+;; FRAME is the frame where the event happened.
+;;
+;; After calling 'llk-init' and setting a non-nil value for
+;; 'enable-low-level-key-events', events begin to be handled by 'llk-handler',
+;; which tries to detect n-taps and calls the corresponding function.
+
+(require 'cl-lib)
+
+;; User options
+
+(defvar llk-bindings nil
+  "Bindings for low level key events (press/release/tap).
+Use the `llk-bind' function to add bindings.  See its documentation for
+a description of the binding information.")
+
+(defvar llk-tap-count 2
+  "Number or key press/releases to consider a tap.")
+
+(defvar llk-tap-timeout 1000
+  "Time in milliseconds between consecutive key presses/releases to
+consider a tap.")
+
+(defvar llk-tap-keys
+  '(xk-shift-l xk-shift-r xk-control-l xk-control-r meta)
+  "Keys that can generate taps.")
+
+(defvar llk-keysyms nil
+  "List of keysym numbers and their corresponding symbols.
+Each element has the form (KEYSYM . SYMBOL).  The variable value for
+each symbol is the keysym.  This list is initialized by `llk-init'.")
+
+(defvar llk-describe-next-press nil
+  "Internal variable to mark that next key press should be described.")
+
+(defmacro define-xk (name x-keysym w32-keysym)
+  "Internal macro to define keysyms."
+  `(let ((ksym (pcase (window-system)
+                 ('pgtk ,x-keysym)
+                 ('x ,x-keysym)
+                 ('w32 ,w32-keysym))))
+     (defconst ,name ksym "Constant for a keysym value.")
+     (push (cons ksym ',name) llk-keysyms)))
+
+(defun llk-define-keysyms ()
+  "Initialize the keysym list, `llk-keysyms'.  Called from `llk-init'."
+  (setq llk-keysyms nil)
+
+  ;; tty keys
+  (define-xk xk-backspace   #xff08 #x08) ;; XK_BackSpace VK_BACK
+  (define-xk xk-tab         #xff09 #x09) ;; XK_Tab VK_TAB
+  (define-xk xk-clear       #xff0b #x0C) ;; XK_Clear VK_CLEAR
+  (define-xk xk-return      #xff0d #x0D) ;; XK_Return VK_RETURN
+  (define-xk xk-pause       #xff13 #x13) ;; XK_Pause VK_PAUSE
+  (define-xk xk-scroll-lock #xff14 #x91) ;; XK_Scroll_Lock VK_SCROLL
+  (define-xk xk-escape      #xff1B #x1B) ;; XK_Escape VK_ESCAPE
+  (define-xk xk-delete      #xffff #x2E) ;; XK_Delete VK_DELETE
+
+  ;; Cursor control and motion
+  (define-xk xk-home        #xff50 #x24) ;; XK_Home VK_HOME
+  (define-xk xk-left        #xff51 #x25) ;; XK_Left VK_LEFT
+  (define-xk xk-up          #xff52 #x26) ;; XK_Up VK_UP
+  (define-xk xk-right       #xff53 #x27) ;; XK_Right VK_RIGHT
+  (define-xk xk-down        #xff54 #x28) ;; XK_Down VK_DOWN
+  (define-xk xk-page-up     #xff55 #x21) ;; XK_Page_Up VK_PRIOR
+  (define-xk xk-page-down   #xff56 #x22) ;; XK_Page_Down VK_NEXT
+  (define-xk xk-end         #xff57 #x23) ;; XK_End VK_END
+  (define-xk xk-begin       #xff58 #x24) ;; XK_Begin VK_HOME
+
+  ;; Special Windows keyboard keys
+  (define-xk xk-win-l       #xFF5B #x5B) ;; XK_Win_L VK_LWIN
+  (define-xk xk-win-r       #xFF5C #x5C) ;; XK_Win_R VK_RWIN
+  (define-xk xk-app         #xFF5D #x5D) ;; XK_App VK_APPS
+
+  ;; Misc functions
+  (define-xk xk-select      #xff60 #x29) ;; XK_Select VK_SELECT
+  (define-xk xk-print       #xff61 #x2A) ;; XK_Print VK_PRINT
+  (define-xk xk-insert      #xff64 #x2D) ;; XK_Insert VK_INSERT
+  (define-xk xk-num-lock    #xff7f #x90) ;; XK_Num_Lock VK_NUMLOCK
+
+  ;; Keypad
+  ;; TODO: Check values for MS-Windows
+  (define-xk xk-kp-enter    #xff8d nil) ;; XK_KP_Enter ???
+  (define-xk xk-kp-multiply #xffaa nil) ;; XK_KP_Multiply ???
+  (define-xk xk-kp-add      #xffab nil) ;; XK_KP_Add ???
+  (define-xk xk-kp-subtract #xffad nil) ;; XK_KP_Subtract ???
+  (define-xk xk-kp-decimal  #xffae nil) ;; XK_KP_Decimal ???
+  (define-xk xk-kp-divide   #xffaf nil) ;; XK_KP_Divide ???
+  (define-xk xk-kp-0        #xffb0 #x60) ;; XK_KP_0 VK_NUMPAD0
+  (define-xk xk-kp-1        #xffb1 #x61) ;; XK_KP_1 VK_NUMPAD1
+  (define-xk xk-kp-2        #xffb2 #x62) ;; XK_KP_2 VK_NUMPAD2
+  (define-xk xk-kp-3        #xffb3 #x63) ;; XK_KP_3 VK_NUMPAD3
+  (define-xk xk-kp-4        #xffb4 #x64) ;; XK_KP_4 VK_NUMPAD4
+  (define-xk xk-kp-5        #xffb5 #x65) ;; XK_KP_5 VK_NUMPAD5
+  (define-xk xk-kp-6        #xffb6 #x66) ;; XK_KP_6 VK_NUMPAD6
+  (define-xk xk-kp-7        #xffb7 #x67) ;; XK_KP_7 VK_NUMPAD7
+  (define-xk xk-kp-8        #xffb8 #x68) ;; XK_KP_8 VK_NUMPAD8
+  (define-xk xk-kp-9        #xffb9 #x69) ;; XK_KP_9 VK_NUMPAD9
+
+  ;; Function keys
+  (define-xk xk-f1          #xffbe #x70) ;; XK_F1 VK_F1
+  (define-xk xk-f2          #xffbf #x71) ;; XK_F2 VK_F2
+  (define-xk xk-f3          #xffc0 #x72) ;; XK_F3 VK_F3
+  (define-xk xk-f4          #xffc1 #x73) ;; XK_F4 VK_F4
+  (define-xk xk-f5          #xffc2 #x74) ;; XK_F5 VK_F5
+  (define-xk xk-f6          #xffc3 #x75) ;; XK_F6 VK_F6
+  (define-xk xk-f7          #xffc4 #x76) ;; XK_F7 VK_F7
+  (define-xk xk-f8          #xffc5 #x77) ;; XK_F8 VK_F8
+  (define-xk xk-f9          #xffc6 #x78) ;; XK_F9 VK_F9
+  (define-xk xk-f10         #xffc7 #x79) ;; XK_F10 VK_F10
+  (define-xk xk-f11         #xffc8 #x7A) ;; XK_F11 VK_F11
+  (define-xk xk-f12         #xffc9 #x7B) ;; XK_F12 VK_F12
+  (define-xk xk-f13         #xffca #x7C) ;; XK_F13 VK_F13
+  (define-xk xk-f14         #xffcb #x7D) ;; XK_F14 VK_F14
+  (define-xk xk-f15         #xffcc #x7E) ;; XK_F15 VK_F15
+  (define-xk xk-f16         #xffcd #x7F) ;; XK_F16 VK_F16
+  (define-xk xk-f17         #xffce #x80) ;; XK_F17 VK_F17
+  (define-xk xk-f18         #xffcf #x81) ;; XK_F18 VK_F18
+  (define-xk xk-f19         #xffd0 #x82) ;; XK_F19 VK_F19
+  (define-xk xk-f20         #xffd1 #x83) ;; XK_F20 VK_F20
+  (define-xk xk-f21         #xffd2 #x84) ;; XK_F21 VK_F21
+  (define-xk xk-f22         #xffd3 #x85) ;; XK_F22 VK_F22
+  (define-xk xk-f23         #xffd4 #x86) ;; XK_F23 VK_F23
+  (define-xk xk-f24         #xffd5 #x87) ;; XK_F24 VK_F24
+
+  ;; Modifier keys
+  (define-xk xk-shift-l     #xffe1 #xA0) ;; XK_Shift_L VK_LSHIFT
+  (define-xk xk-shift-r     #xffe2 #xA1) ;; XK_Shift_R VK_RSHIFT
+  (define-xk xk-control-l   #xffe3 #xA2) ;; XK_Control_L VK_LCONTROL
+  (define-xk xk-control-r   #xffe4 #xA3) ;; XK_Control_R VK_RCONTROL
+  (define-xk xk-caps-lock   #xffe5 #x14) ;; XK_Caps_Lock VK_CAPITAL
+  (define-xk xk-metal-l     #xffe7 nil) ;; XK_Meta_L
+  (define-xk xk-metal-t     #xffee nil) ;; XK_Meta_R
+  (define-xk xk-alt-l       #xffe9 #xA4) ;; XK_Alt_L VK_LMENU
+  (define-xk xk-alt-r       #xffea #xA5) ;; XK_Alt_R VK_RMENU
+  (define-xk xk-super-l     #xffeb nil) ;; XK_Super_L
+  (define-xk xk-super-r     #xffec nil) ;; XK_Super_R
+  (define-xk xk-hyper-l     #xffed nil) ;; XK_Hyper_L
+  (define-xk xk-hyper-r     #xffee nil) ;; XK_Hyper_R
+
+  ;; Latin 1
+  ;; For numbers and letters, MS-Windows does not define constant names.
+  ;; X11 defines distinct keysyms for lowercase and uppercase
+  ;; letters. We use only the uppercase ones. Events with lowercase
+  ;; letters are converted to uppercase.
+  (define-xk xk-space       #x0020 #x20) ;; XK_space VK_SPACE
+  (define-xk xk-0           #x0030 #x30) ;; XK_0
+  (define-xk xk-1           #x0031 #x31) ;; XK_1
+  (define-xk xk-2           #x0032 #x32) ;; XK_2
+  (define-xk xk-3           #x0033 #x33) ;; XK_3
+  (define-xk xk-4           #x0034 #x34) ;; XK_4
+  (define-xk xk-5           #x0035 #x35) ;; XK_5
+  (define-xk xk-6           #x0036 #x36) ;; XK_6
+  (define-xk xk-7           #x0037 #x37) ;; XK_7
+  (define-xk xk-8           #x0038 #x38) ;; XK_8
+  (define-xk xk-9           #x0039 #x39) ;; XK_9
+  (define-xk xk-a           #x0041 #x41) ;; XK_A
+  (define-xk xk-b           #x0042 #x42) ;; XK_B
+  (define-xk xk-c           #x0043 #x43) ;; XK_C
+  (define-xk xk-d           #x0044 #x44) ;; XK_D
+  (define-xk xk-e           #x0045 #x45) ;; XK_E
+  (define-xk xk-f           #x0046 #x46) ;; XK_F
+  (define-xk xk-g           #x0047 #x47) ;; XK_G
+  (define-xk xk-h           #x0048 #x48) ;; XK_H
+  (define-xk xk-i           #x0049 #x49) ;; XK_I
+  (define-xk xk-j           #x004A #x4A) ;; XK_J
+  (define-xk xk-k           #x004B #x4B) ;; XK_K
+  (define-xk xk-l           #x004C #x4C) ;; XK_L
+  (define-xk xk-m           #x004D #x4D) ;; XK_M
+  (define-xk xk-n           #x004E #x4E) ;; XK_N
+  (define-xk xk-o           #x004F #x4F) ;; XK_O
+  (define-xk xk-p           #x0050 #x50) ;; XK_P
+  (define-xk xk-q           #x0051 #x51) ;; XK_Q
+  (define-xk xk-r           #x0052 #x52) ;; XK_R
+  (define-xk xk-s           #x0053 #x53) ;; XK_S
+  (define-xk xk-t           #x0054 #x54) ;; XK_T
+  (define-xk xk-u           #x0055 #x55) ;; XK_U
+  (define-xk xk-v           #x0056 #x56) ;; XK_V
+  (define-xk xk-w           #x0057 #x57) ;; XK_W
+  (define-xk xk-x           #x0058 #x58) ;; XK_X
+  (define-xk xk-y           #x0059 #x59) ;; XK_Y
+  (define-xk xk-z           #x005A #x5A));; XK_Z
+
+(defun llk-init ()
+  "Initialize low-level key events.
+Fills the `llk-keysyms' list, and binds the `low-level-key' event
+to the `llk-handle' function.  Resets the `llk-bindings' list.
+Besides calling this function, you need to set `enable-low-level-key-events'
+to a non-nil value"
+  (interactive)
+  (llk-define-keysyms)
+  (define-key special-event-map [low-level-key] 'llk-handle)
+  (setq llk-bindings nil))
+
+(defsubst event-is-key-press (event)
+  "Return the value of the IS-KEY-PRESS field of the EVENT, a low level key event."
+  (declare (side-effect-free t))
+  (if (consp event) (nth 1 event)))
+
+(defsubst event-keysym (event)
+  "Return the value of the KEY field of the EVENT, a low level key event."
+  (declare (side-effect-free t))
+  (if (consp event) (nth 2 event)))
+
+(defsubst event-modifier (event)
+  "Return the value of the MODIFIER field of the EVENT, a low level key event."
+  (declare (side-effect-free t))
+  (if (consp event) (nth 3 event)))
+
+(defsubst event-time (event)
+  "Return the value of the TIME field of the EVENT, a low level key event."
+  (declare (side-effect-free t))
+  (if (consp event) (nth 4 event)))
+
+;; For example:
+;; Bind key tap to command
+;;    (llk-bind 'tap 'xk-shift-l 'delete-other-windows)
+;; Bind modifiry tap to command
+;;     (llk-bind 'tap 'shift 'delete-other-windows)
+;; Bind tap to hyper modifier
+;;      (llk-bind 'tap 'xk-shift-r (lambda ()
+;;                              (message "H-...")
+;;                              (setq unread-command-events
+;;                                    (append (event-apply-hyper-modifier nil) nil))))
+;; Can bind to a command or function
+(defun llk-bind (action key function)
+  "Bind a command a function to a low level key event.
+The only action supported currently is `tap'. The key can be a keysym
+symbol, or a modifier symbol (shift, control, alt, meta, hyper, super).
+If there is no keysym symbol for a key, use the keysym number.  "
+  (push (list action key function) llk-bindings))
+
+;; We store the last events (key/modifier is-press timestamp) here to
+;; test for multitap.
+(defvar llk-events nil
+  "Internal variable for detecting taps.")
+
+;; If positive, return key (xk-shift-l, etc) else return nil.
+(defun llk-detect-n-tap (n timeout)
+  "Internal function to detect n-tap keys."
+  (let (key
+        (is-press (event-is-key-press last-input-event))
+        ;; convert number to keysym symbol
+        (keysym (cdr (assoc (event-keysym last-input-event) llk-keysyms)))
+        (timestamp (event-time last-input-event))
+        (modifier (event-modifier last-input-event)))
+
+    ;; if ehte is no symbol for this key, use its keysym number
+    (unless keysym (setq keysym (event-keysym last-input-event)))
+
+    ;; look in llk-tap-keys for the key, then the modifier
+    (if (member keysym llk-tap-keys)
+        (setq key keysym)
+      (if (member modifier llk-tap-keys)
+          (setq key modifier)))
+
+    (if (not key)
+        ;; Key not in tap list, clear history
+        (setq llk-events nil)
+      ;; Clear it also if the first element is from a different key
+      (and llk-events
+           (not (equal (car (car llk-events)) key))
+           (setq llk-events nil))
+      (push (list key is-press timestamp) llk-events)
+      ;; Only care about last 2xN events
+      (ntake (* 2 n) llk-events)
+      ;; If we have:
+      ;; - Exactly 2 * n events.
+      ;; - down, up, down, up, ...
+      ;; - not two much time between first and last
+      (and (eq (* 2 n) (length llk-events))
+           (cl-every 'eq
+                     (ntake (* 2 n)
+                            (list nil t nil t nil t nil t
+                                  nil t nil t nil t nil t))
+                     (mapcar 'cl-second llk-events))
+           (< (- (cl-third (cl-first llk-events))
+                 (cl-third (car (last llk-events))))
+              timeout)
+           (progn
+             (setq llk-events nil)
+             key)))))
+
+(defun describe-low-level-key ()
+  "Wait for the next key press and describe the low level key event it
+generates."
+  (interactive)
+  (setq llk-describe-next-press t))
+
+(defun llk-show-event-description ()
+  "Shoe information about the last low level key event."
+  (setq llk-describe-next-press nil)
+  (with-help-window (help-buffer)
+    (insert "\n")
+    (let* ((xk (event-keysym last-input-event))
+           (sym (assoc xk llk-keysyms)))
+      (insert (format "Keysym number: %d (#x%X),\n" xk xk))
+      (if sym
+          (insert (format "which corresponds to named key %s.\n\n" (cdr sym)))
+        (insert "which does not correspond to any known named key.\n\n"))
+      (if (event-modifier last-input-event)
+          (insert (format "This key corresponds to the %s modifier.\n\n"
+                          (event-modifier last-input-event)))
+        (insert "This key does not correspond to a modifier.\n\n"))
+      (insert "See the value of the `llk-keysyms' variable for a list of known keys.\n"))))
+
+(defun llk-handle ()
+  "Internal function to handle low level key events."
+  (interactive)
+  (if (and (event-is-key-press last-input-event)
+           llk-describe-next-press)
+      (llk-show-event-description)
+    (let ((tap-key (llk-detect-n-tap
+                    llk-tap-count
+                    llk-tap-timeout)))
+      (when tap-key
+        (let ((func (cl-third
+                     (seq-find
+                      (lambda (b)
+                        (and (eq (cl-first b) 'tap)
+                             (eq (cl-second b) tap-key)))
+                      llk-bindings))))
+          (cond
+           ((commandp func) (call-interactively func))
+           ((functionp func) (funcall func))))))))
diff --git a/src/gtkutil.c b/src/gtkutil.c
index d57627f152f..86d4321cf35 100644
--- a/src/gtkutil.c
+++ b/src/gtkutil.c
@@ -98,6 +98,7 @@ G_DEFINE_TYPE (EmacsMenuBar, emacs_menu_bar, GTK_TYPE_MENU_BAR)
 static void xg_im_context_preedit_changed (GtkIMContext *, gpointer);
 static void xg_im_context_preedit_end (GtkIMContext *, gpointer);
 static bool xg_widget_key_press_event_cb (GtkWidget *, GdkEvent *, gpointer);
+static bool xg_widget_key_release_event_cb (GtkWidget *, GdkEvent *, gpointer);
 #endif
 
 #if GTK_CHECK_VERSION (3, 10, 0)
@@ -1749,6 +1750,12 @@ xg_create_frame_widgets (struct frame *f)
   g_signal_connect (G_OBJECT (wfixed), "key-press-event",
 		    G_CALLBACK (xg_widget_key_press_event_cb),
 		    NULL);
+
+  g_signal_connect (G_OBJECT (wfixed), "key-release-event",
+		    G_CALLBACK (xg_widget_key_release_event_cb),
+		    NULL);
+
+
 #endif
 
   {
@@ -6376,6 +6383,53 @@ xg_im_context_preedit_end (GtkIMContext *imc, gpointer user_data)
   kbd_buffer_store_event (&inev);
 }
 
+#ifndef HAVE_XINPUT2
+static void
+xg_maybe_send_low_level_key_event (struct frame *f,
+				   GdkEvent *xev)
+{
+  GdkEventKey xkey = xev->key;
+  bool is_press;
+  Lisp_Object key, modifier;
+  union buffered_input_event inev;
+
+  if (NILP (Venable_low_level_key_events))
+    return;
+
+  switch (xev->type)
+    {
+    case GDK_KEY_PRESS:
+      is_press = true;
+      break;
+    case GDK_KEY_RELEASE:
+      is_press = false;
+      break;
+    default:
+      return;
+    }
+
+  modifier = x_get_modifier_for_keycode (FRAME_OUTPUT_DATA (f)->display_info,
+					 xev->key.hardware_keycode);
+
+  int keysym = xkey.keyval;
+
+  if (keysym >= GDK_KEY_a && keysym <= GDK_KEY_z)
+    keysym -= GDK_KEY_a - GDK_KEY_A;
+
+  if (!kbd_low_level_key_is_enabled (keysym, modifier))
+    return;
+
+  key = make_fixnum (keysym);
+
+  EVENT_INIT (inev.ie);
+  XSETFRAME (inev.ie.frame_or_window, f);
+  inev.ie.kind = LOW_LEVEL_KEY_EVENT;
+  inev.ie.timestamp = xkey.time;
+  inev.ie.arg = list3 (is_press ? Qt : Qnil, key, modifier);
+  kbd_buffer_store_buffered_event (&inev, &xg_pending_quit_event);
+}
+#endif
+
 static bool
 xg_widget_key_press_event_cb (GtkWidget *widget, GdkEvent *event,
 			      gpointer user_data)
@@ -6404,6 +6458,10 @@ xg_widget_key_press_event_cb (GtkWidget *widget, GdkEvent *event,
   if (!f)
     return true;
 
+#ifndef HAVE_XINPUT2
+    xg_maybe_send_low_level_key_event (f, event);
+#endif
+
   if (popup_activated ())
     return true;
 
@@ -6557,6 +6615,29 @@ xg_widget_key_press_event_cb (GtkWidget *widget, GdkEvent *event,
   return true;
 }
 
+static bool
+xg_widget_key_release_event_cb (GtkWidget *widget, GdkEvent *event,
+				gpointer user_data)
+{
+#ifndef HAVE_XINPUT2
+  Lisp_Object tail, tem;
+  struct frame *f = NULL;
+
+  FOR_EACH_FRAME (tail, tem)
+    {
+      if (FRAME_X_P (XFRAME (tem))
+	  && (FRAME_GTK_WIDGET (XFRAME (tem)) == widget))
+	{
+	  f = XFRAME (tem);
+	  break;
+	}
+    }
+  if (f)
+    xg_maybe_send_low_level_key_event (f, event);
+#endif
+  return true;
+}
+
 bool
 xg_filter_key (struct frame *frame, XEvent *xkey)
 {
diff --git a/src/keyboard.c b/src/keyboard.c
index 6d28dca9aeb..442bf7cee34 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -4274,6 +4274,7 @@ kbd_buffer_get_event (KBOARD **kbp,
       case CONFIG_CHANGED_EVENT:
       case FOCUS_OUT_EVENT:
       case SELECT_WINDOW_EVENT:
+      case LOW_LEVEL_KEY_EVENT:
         {
           obj = make_lispy_event (&event->ie);
           kbd_fetch_ptr = next_kbd_event (event);
@@ -7118,12 +7119,47 @@ make_lispy_event (struct input_event *event)
     case PREEDIT_TEXT_EVENT:
       return list2 (Qpreedit_text, event->arg);
 
+    case LOW_LEVEL_KEY_EVENT:
+      return listn (6, Qlow_level_key,
+		    XCAR (event->arg), /* Press or release.  */
+		    XCAR (XCDR (event->arg)), /* The key symbol.  */
+		    XCAR (XCDR (XCDR (event->arg))), /* The modifier.  */
+		    make_fixnum (event->timestamp),
+		    event->frame_or_window);
+
       /* The 'kind' field of the event is something we don't recognize.  */
     default:
       emacs_abort ();
     }
 }
 
+bool
+kbd_low_level_key_is_enabled (int keysym, Lisp_Object modifier)
+{
+  if (Venable_low_level_key_events == Qt)
+    return true;
+
+  if (Venable_low_level_key_events == Qnil)
+    return false;
+
+  if (FIXNUMP (Venable_low_level_key_events))
+    return keysym == XFIXNUM (Venable_low_level_key_events);
+
+  if (Venable_low_level_key_events == Qmodifiers)
+    return modifier != Qnil;
+
+  for (Lisp_Object e = Venable_low_level_key_events; CONSP (e); e = XCDR (e))
+    {
+      Lisp_Object c = XCAR (e);
+      if (FIXNUMP (c) && XFIXNUM (c) == keysym)
+	return true;
+      if (c == Qmodifiers && modifier != Qnil)
+	return true;
+    }
+
+  return false;
+}
+
 static Lisp_Object
 make_lispy_movement (struct frame *frame, Lisp_Object bar_window, enum scroll_bar_part part,
 		     Lisp_Object x, Lisp_Object y, Time t)
@@ -12931,6 +12967,29 @@ syms_of_keyboard (void)
   DEFSYM (Qfile_notify, "file-notify");
 #endif /* USE_FILE_NOTIFY */
 
+
+  DEFSYM (Qmodifiers, "modifiers");
+
+  DEFVAR_LISP ("enable-low-level-key-events", Venable_low_level_key_events,
+	       doc: /* If non-nil, reception of low-level key events is enabled.
+
+The value configures the set of keys that are handled:
+
+If t, send events for all keys.
+
+If a number, send events for the corresponding keysym.  When calling
+'llk-init', a set of variables with the xk- prefix is initialized with
+the numeric values for keysyms.  This numbers are platform dependent.
+
+If a symbol, a predefined set of keys is selected.  The only currently
+valid symbol is 'modifiers.
+
+If a list of numbers and/or symbols, the corresponding keysyms and sets
+are selected.  */);
+  Venable_low_level_key_events = Qnil;
+
+  DEFSYM (Qlow_level_key, "low-level-key");
+
   DEFSYM (Qtouch_end, "touch-end");
 
   /* Menu and tool bar item parts.  */
@@ -14018,6 +14077,8 @@ keys_of_keyboard (void)
 			    "handle-focus-out");
   initial_define_lispy_key (Vspecial_event_map, "move-frame",
 			    "handle-move-frame");
+  initial_define_lispy_key (Vspecial_event_map, "low-level-key",
+			    "ignore");
 }
 
 /* Mark the pointers in the kboard objects.
diff --git a/src/keyboard.h b/src/keyboard.h
index 387501c9f88..83f9a0f141a 100644
--- a/src/keyboard.h
+++ b/src/keyboard.h
@@ -511,6 +511,7 @@ kbd_buffer_store_event_hold (struct input_event *event,
 extern Lisp_Object menu_item_eval_property (Lisp_Object);
 extern bool kbd_buffer_events_waiting (void);
 extern void add_user_signal (int, const char *);
+extern bool kbd_low_level_key_is_enabled (int, Lisp_Object);
 
 extern int tty_read_avail_input (struct terminal *, struct input_event *);
 extern struct timespec timer_check (void);
diff --git a/src/pgtkterm.c b/src/pgtkterm.c
index 079945126e0..fba81f5ec0e 100644
--- a/src/pgtkterm.c
+++ b/src/pgtkterm.c
@@ -5201,6 +5201,56 @@ pgtk_enqueue_preedit (struct frame *f, Lisp_Object preedit)
   evq_enqueue (&inev);
 }
 
+static void
+pgtk_maybe_send_low_level_key_event (struct frame *f, GdkEvent *event)
+{
+  GdkEventKey xkey = event->key;
+  bool is_press;
+  Lisp_Object key, modifier;
+  union buffered_input_event inev;
+
+  if (NILP (Venable_low_level_key_events))
+    return;
+
+  switch (event->type)
+    {
+    case GDK_KEY_PRESS:
+      is_press = true;
+      break;
+    case GDK_KEY_RELEASE:
+      is_press = false;
+      break;
+    default:
+      return;
+    }
+
+  /* We don't support modifier identification on PGTK. We only can tell
+    if the key corresponds to a modifier or not, which is used for
+    filtering enabled keys with kbd_low_level_key_is_enabled.  */
+  modifier = event->key.is_modifier ? Qt : Qnil;
+
+  int keysym = xkey.keyval;
+
+  if (keysym >= GDK_KEY_a && keysym <= GDK_KEY_z)
+    keysym -= GDK_KEY_a - GDK_KEY_A;
+  if (!kbd_low_level_key_is_enabled (keysym, modifier))
+    return;
+
+  if (!f)
+    f = pgtk_any_window_to_frame (event->key.window);
+  if (!f)
+    return;
+
+  key = make_fixnum (keysym);
+
+  EVENT_INIT (inev.ie);
+  XSETFRAME (inev.ie.frame_or_window, f);
+  inev.ie.kind = LOW_LEVEL_KEY_EVENT;
+  inev.ie.timestamp = event->key.time;
+  inev.ie.arg = list3 (is_press ? Qt : Qnil, key, modifier);
+  evq_enqueue (&inev);
+}
+
 static gboolean
 key_press_event (GtkWidget *widget, GdkEvent *event, gpointer *user_data)
 {
@@ -5211,6 +5261,9 @@ key_press_event (GtkWidget *widget, GdkEvent *event, gpointer *user_data)
   struct pgtk_display_info *dpyinfo;
 
   f = pgtk_any_window_to_frame (gtk_widget_get_window (widget));
+
+  pgtk_maybe_send_low_level_key_event (f, event);
+
   EVENT_INIT (inev.ie);
   hlinfo = MOUSE_HL_INFO (f);
   nbytes = 0;
@@ -5454,6 +5507,8 @@ key_release_event (GtkWidget *widget,
   GdkDisplay *display;
   struct pgtk_display_info *dpyinfo;
 
+  pgtk_maybe_send_low_level_key_event (NULL, event);
+
   display = gtk_widget_get_display (widget);
   dpyinfo = pgtk_display_info_for_display (display);
 
diff --git a/src/termhooks.h b/src/termhooks.h
index d6a9300bac9..966a6492f69 100644
--- a/src/termhooks.h
+++ b/src/termhooks.h
@@ -347,6 +347,7 @@ #define EMACS_TERMHOOKS_H
   /* In a NOTIFICATION_EVENT, .arg is a lambda to evaluate.  */
   , NOTIFICATION_EVENT
 #endif /* HAVE_ANDROID */
+  , LOW_LEVEL_KEY_EVENT
 };
 
 /* Bit width of an enum event_kind tag at the start of structs and unions.  */
diff --git a/src/w32fns.c b/src/w32fns.c
index e2455b9271e..1d18bf408e1 100644
--- a/src/w32fns.c
+++ b/src/w32fns.c
@@ -4669,6 +4669,11 @@ w32_wnd_proc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
     case WM_KEYUP:
     case WM_SYSKEYUP:
       record_keyup (wParam, lParam);
+      if (!NILP (Venable_low_level_key_events))
+	{
+	  signal_user_input ();
+	  my_post_msg (&wmsg, hwnd, WM_EMACS_LOW_LEVEL_KEY, wParam, lParam);
+	}
       goto dflt;
 
     case WM_KEYDOWN:
@@ -4695,6 +4700,12 @@ w32_wnd_proc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
       if (w32_use_fallback_wm_chars_method)
 	wParam = map_keypad_keys (wParam, (lParam & 0x1000000L) != 0);
 
+      if (!NILP (Venable_low_level_key_events))
+	{
+	  signal_user_input ();
+	  my_post_msg (&wmsg, hwnd, WM_EMACS_LOW_LEVEL_KEY, wParam, lParam);
+	}
+
       windows_translate = 0;
 
       switch (wParam)
diff --git a/src/w32term.c b/src/w32term.c
index e18f39dd2a8..a2e0a4b0fa0 100644
--- a/src/w32term.c
+++ b/src/w32term.c
@@ -5270,6 +5270,55 @@ w32_read_socket (struct terminal *terminal,
 	    }
 	  break;
 
+	case WM_EMACS_LOW_LEVEL_KEY:
+	  WORD key_flags = HIWORD (msg.msg.lParam);
+	  BOOL is_wm_keyup = key_flags & KF_UP;
+
+	  if (is_wm_keyup || (key_flags & KF_REPEAT) == 0) /* WM_KEYDOWN, not repeating.  */
+	    {
+	      WORD scan_code = LOBYTE (key_flags);
+	      if (key_flags & KF_EXTENDED)
+		scan_code = MAKEWORD (scan_code, 0xE0);
+
+	      UINT translated = MapVirtualKey (scan_code, MAPVK_VSC_TO_VK_EX);
+	      WORD vk = LOWORD (msg.msg.wParam);
+	      if (translated)
+		vk = LOWORD (translated);
+
+	      Lisp_Object key = make_fixnum (vk);
+	      Lisp_Object modifier = Qnil;
+
+	      switch (vk)
+		{
+		case VK_LSHIFT:
+		case VK_RSHIFT:
+		  modifier = Qshift;
+		  break;
+		case VK_LCONTROL:
+		case VK_RCONTROL:
+		  modifier = Qctrl;
+		  break;
+		case VK_LMENU:
+		case VK_RMENU:
+		  modifier = Qmeta;
+		  break;
+		}
+
+	      if (kbd_low_level_key_is_enabled (vk, modifier))
+		{
+		  f = w32_window_to_frame (dpyinfo, msg.msg.hwnd);
+		  inev.kind = LOW_LEVEL_KEY_EVENT;
+		  XSETFRAME (inev.frame_or_window, f);
+		  inev.timestamp = msg.msg.time;
+		  inev.arg = list3 (is_wm_keyup ? Qnil : Qt, key, modifier);
+		  kbd_buffer_store_event_hold (&inev, hold_quit);
+		}
+
+	      inev.kind = NO_EVENT;
+
+	    }
+	  break;
+
         case WM_UNICHAR:
 	case WM_SYSCHAR:
 	case WM_CHAR:
diff --git a/src/w32term.h b/src/w32term.h
index cad9fcf8cb1..88f7dfeef8b 100644
--- a/src/w32term.h
+++ b/src/w32term.h
@@ -713,7 +713,8 @@ #define WM_EMACS_FILENOTIFY            (WM_EMACS_START + 25)
 #define WM_EMACS_IME_STATUS            (WM_EMACS_START + 26)
 #define WM_EMACS_DRAGOVER              (WM_EMACS_START + 27)
 #define WM_EMACS_DROP                  (WM_EMACS_START + 28)
-#define WM_EMACS_END                   (WM_EMACS_START + 29)
+#define WM_EMACS_LOW_LEVEL_KEY         (WM_EMACS_START + 29)
+#define WM_EMACS_END                   (WM_EMACS_START + 30)
 
 #define WND_FONTWIDTH_INDEX    (0)
 #define WND_LINEHEIGHT_INDEX   (4)
diff --git a/src/xterm.c b/src/xterm.c
index 0c20d38b0f7..72715e0ed73 100644
--- a/src/xterm.c
+++ b/src/xterm.c
@@ -17840,6 +17840,141 @@ #define STORE_KEYSYM_FOR_DEBUG(keysym) ((void)0)
 
 static struct x_display_info *next_noop_dpyinfo;
 
+Lisp_Object
+x_get_modifier_for_keycode (struct x_display_info *dpyinfo,
+			    int keycode)
+{
+#ifdef HAVE_XKB
+  if (dpyinfo->xkb_desc)
+    for (int mod = 0; mod < XkbNumModifiers; mod++)
+      {
+	int mask = (1 << mod);
+	if (dpyinfo->xkb_desc->map->modmap[keycode] & mask)
+	  {
+	    if (mask == ShiftMask)
+	      return Qshift;
+	    if (mask == ControlMask)
+	      return Qctrl;
+	    if (mask == dpyinfo->meta_mod_mask)
+	      return Qmeta;
+	    if (mask == dpyinfo->alt_mod_mask)
+	      return Qalt;
+	    if (mask == dpyinfo->super_mod_mask)
+	      return Qsuper;
+	    if (mask == dpyinfo->hyper_mod_mask)
+	      return Qhyper;
+	  }
+      }
+#endif
+  XModifierKeymap *map = dpyinfo->modmap;
+  if (map)
+    for (int mod = 0; mod < 8; mod++)
+      {
+	int mask = (1 << mod);
+        for (int key = 0; key < map->max_keypermod; key++)
+	  if (map->modifiermap[mod * map->max_keypermod + key] == keycode)
+	    {
+	      if (mask == ShiftMask)
+		return Qshift;
+	      if (mask == ControlMask)
+		return Qctrl;
+	      if (mask == dpyinfo->meta_mod_mask)
+		return Qmeta;
+	      if (mask == dpyinfo->alt_mod_mask)
+		return Qalt;
+	      if (mask == dpyinfo->super_mod_mask)
+		return Qsuper;
+	      if (mask == dpyinfo->hyper_mod_mask)
+		return Qhyper;
+	    }
+      }
+  return Qnil;
+}
+
+static void
+x_maybe_send_low_level_key_event (struct x_display_info *dpyinfo,
+				  const XEvent *xev, struct frame *f)
+{
+  XKeyEvent xkey;
+  bool is_press;
+  KeySym keysym;
+  Lisp_Object key, modifier;
+  struct input_event ie;
+
+  if (NILP (Venable_low_level_key_events))
+    return;
+
+  switch (xev->type)
+    {
+    case KeyPress:
+      is_press = true;
+      xkey = xev->xkey;
+      break;
+    case KeyRelease:
+      is_press = false;
+      xkey = xev->xkey;
+      break;
+#ifdef HAVE_XINPUT2
+    case GenericEvent:
+      XIDeviceEvent *xiev = xev->xcookie.data;
+      switch (xev->xgeneric.evtype)
+	{
+	case XI_KeyPress:
+	  is_press = true;
+	  break;
+	case XI_KeyRelease:
+	  is_press = false;
+	  break;
+	default:
+	  return;
+	}
+
+      xkey.serial = xiev->serial;
+      xkey.send_event = xiev->send_event;
+      xkey.display = xiev->display;
+      xkey.window = xiev->event;
+      xkey.root = xiev->root;
+      xkey.subwindow = xiev->child;
+      xkey.time = xiev->time;
+      xkey.x = xiev->event_x;
+      xkey.y = xiev->event_y;
+      xkey.x_root = xiev->root_x;
+      xkey.y_root = xiev->root_y;
+      xkey.state = xiev->mods.effective;
+      xkey.keycode = xiev->detail;
+      xkey.same_screen = 1;
+      break;
+#endif
+    default:
+      return;
+    }
+
+  if (!f)
+    f = x_any_window_to_frame (dpyinfo, xkey.window);
+  if (!f)
+    return;
+
+  XLookupString (&xkey, NULL, 0, &keysym, NULL);
+
+  modifier = x_get_modifier_for_keycode (dpyinfo, xkey.keycode);
+
+  /* Convert lowercase latin letter to uppercase.  */
+  if (keysym >= XK_a && keysym <= XK_z)
+    keysym -= XK_a - XK_A;
+
+  if (!kbd_low_level_key_is_enabled (keysym, modifier))
+    return;
+
+  key = make_fixnum (keysym);
+
+  EVENT_INIT (ie);
+  XSETFRAME (ie.frame_or_window, f);
+  ie.kind = LOW_LEVEL_KEY_EVENT;
+  ie.timestamp = xkey.time;
+  ie.arg = list3 (is_press ? Qt : Qnil, key, modifier);
+  kbd_buffer_store_event (&ie);
+}
+
 /* Filter events for the current X input method.
    DPYINFO is the display this event is for.
    EVENT is the X event to filter.
@@ -20206,6 +20341,7 @@ handle_one_xevent (struct x_display_info *dpyinfo,
       goto OTHER;
 
     case KeyPress:
+      x_maybe_send_low_level_key_event (dpyinfo, event, any);
       x_display_set_last_user_time (dpyinfo, event->xkey.time,
 				    event->xkey.send_event,
 				    true);
@@ -20715,6 +20851,7 @@ handle_one_xevent (struct x_display_info *dpyinfo,
 #endif
 
     case KeyRelease:
+      x_maybe_send_low_level_key_event (dpyinfo, event, any);
 #ifdef HAVE_X_I18N
       /* Don't dispatch this event since XtDispatchEvent calls
          XFilterEvent, and two calls in a row may freeze the
@@ -23970,6 +24107,8 @@ handle_one_xevent (struct x_display_info *dpyinfo,
 	      struct xi_device_t *device, *source;
 	      XKeyPressedEvent xkey;
 
+	      x_maybe_send_low_level_key_event (dpyinfo, event, any);
+
 	      coding = Qlatin_1;
 
 	      /* The code under this label is quite desultory.  There
@@ -24586,6 +24725,8 @@ handle_one_xevent (struct x_display_info *dpyinfo,
 #endif
 
 	  case XI_KeyRelease:
+	    x_maybe_send_low_level_key_event (dpyinfo, event, any);
+
 #if defined HAVE_X_I18N || defined USE_GTK || defined USE_LUCID
 	    {
 	      XKeyPressedEvent xkey;
@@ -32662,6 +32803,7 @@ syms_of_xterm (void)
   Vx_toolkit_scroll_bars = Qnil;
 #endif
 
+  DEFSYM (Qshift, "shift");
   DEFSYM (Qmodifier_value, "modifier-value");
   DEFSYM (Qctrl, "ctrl");
   Fput (Qctrl, Qmodifier_value, make_fixnum (ctrl_modifier));
diff --git a/src/xterm.h b/src/xterm.h
index 8d5c9917749..66e052e7acd 100644
--- a/src/xterm.h
+++ b/src/xterm.h
@@ -1906,6 +1906,8 @@ x_mutable_colormap (XVisualInfo *visual)
 extern void tear_down_x_back_buffer (struct frame *f);
 extern void initial_set_up_x_back_buffer (struct frame *f);
 
+extern Lisp_Object x_get_modifier_for_keycode (struct x_display_info *, int);
+
 /* Defined in xfns.c.  */
 extern void x_real_positions (struct frame *, int *, int *);
 extern void x_change_tab_bar_height (struct frame *, int);
-- 
2.35.1.windows.2


  reply	other threads:[~2024-12-02 16:54 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <fb50ec11-7aec-481e-8a3a-ecdcf22eb7c0@imayhem.com>
     [not found] ` <31bdc55d-8c13-4de0-9cef-bd6cc4fb033f@imayhem.com>
     [not found]   ` <s1r8qtzsvbe.fsf@yahoo.com>
2024-11-18 20:35     ` bug#74423: Low level key events Cecilio Pardo
2024-11-18 23:49       ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-11-23 12:08         ` Cecilio Pardo
2024-11-19 15:29       ` Eli Zaretskii
2024-11-19 16:43       ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-11-19 20:05         ` Cecilio Pardo
2024-11-20  4:21           ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-12-02 16:54             ` Cecilio Pardo [this message]
2024-12-04 20:01               ` Eli Zaretskii
2024-12-04 21:25                 ` Cecilio Pardo
2024-12-05  5:41                   ` Eli Zaretskii
2024-12-06  1:01                     ` Po Lu via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-12-07 21:52                       ` Cecilio Pardo

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.gnu.org/software/emacs/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=b0b914a0-b454-41b0-aa3d-d9e243c210f5@imayhem.com \
    --to=cpardo@imayhem.com \
    --cc=74423@debbugs.gnu.org \
    --cc=eliz@gnu.org \
    --cc=luangruo@yahoo.com \
    --cc=monnier@iro.umontreal.ca \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).