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
next prev parent 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).