Subject: bug#74423: Low level key events

From: Cecilio Pardo <[email-masked]>
Date: Mon, 18 Nov 2024 21:35:40 +0100

Here is a still incomplete patch for this, renamed from "physical 
keyboard events", following up to
https://lists.gnu.org/archive/html/emacs-devel/2024-11/msg00085.html

It provides events for press/release of keys, independently of the 
normal keyboard events. These events are bound int the 
special-event-map. Some lisp is included to implement detection of 
multiple tapping on keys, and running commands or simulating modifiers.

Currently, only modifier keys are available, because other keys would 
send their own normal event. This could change when an use case for 
other keys appears.

To provide events for 'logical' modifiers Ctrl, Meta, etc., now there is 
a new event low-level-modifier, in addition to low-level-key. If you 
press Shift_L, two events will be generated, one for 'shift as 
low-level-modifier, and one for 'lshift as low-level-key. The lisp part 
handles those events separately.

I added a variable that enables/disables everything, 
enable-low-level-key-events.

This are some of Po Lu's notes on the previous patch:

> [...] Emacs must not depend on XKB for such a basic feature.

Fixed.

> Please find some means of generating such events by saving them into the
> keyboard event buffer defined in handle_ome_xevent.

As we may generate two events, that is not possible.

> x_filter_events is not the proper location for this call.  It is only
> invoked to filter an event through the active GTK or X input method
> context, and it is not invoked if Emacs is configured not to open input
> method contexts, or they are unavailable by happenstance.

Done. If XINPUT is enabled, the XI_KeyPress event used, over the gtk 
one. If not, the gtk event is used unless there is no input method. In 
this case KeyPress event is used.

The pgtk version is not updated.

From d2403d9fa14b498263abb242324c21441a4246aa Mon Sep 17 00:00:00 2001
From: Cecilio Pardo <[email-masked]>
Date: Mon, 28 Oct 2024 23:57:35 +0100
Subject: [PATCH] Add events for key press and key release on gui systems.

And detection of double/triple taps on modifier keys.

* lisp/low-level-key.el (llk-tap-timeout):
(llk-tap-count):
(llk-tap-keys):
(llk-bindings):
(llk-modifier-bindings):
(llk-init):
(llk-bind):
(llk-modifier-bind):
(llk-events):
(llk-modifier-events):
(llm-handle):
* src/gtkutil.c (xg_create_frame_widgets):
(xg_maybe_send_low_level_key_event):
(xg_widget_key_press_event_cb):
(xg_widget_key_release_event_cb):
* src/xterm.c (x_maybe_send_physical_key_event):
(x_filter_event):
(handle_one_xevent):
(syms_of_xterm):
---
 lisp/low-level-key.el | 141 +++++++++++++++++++++++++++++++++++++++++
 src/gtkutil.c         | 133 ++++++++++++++++++++++++++++++++++++++
 src/keyboard.c        |  38 +++++++++++
 src/pgtkterm.c        |  37 +++++++++++
 src/termhooks.h       |   2 +
 src/w32fns.c          |  11 ++++
 src/w32term.c         |  69 ++++++++++++++++++++
 src/w32term.h         |   3 +-
 src/xterm.c           | 144 ++++++++++++++++++++++++++++++++++++++++++
 9 files changed, 577 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..639c082da15
--- /dev/null
+++ b/lisp/low-level-key.el
@@ -0,0 +1,141 @@
+;;; -*- lexical-binding: t -*-
+(require 'cl-lib)
+
+;; User options
+(defvar llk-tap-timeout 1000)
+(defvar llk-tap-count 2)
+(defvar llk-tap-keys
+  '(lshift rshift lctrl rctrl lalt ralt shift ctrl alt))
+(defvar llk-bindings nil)
+(defvar llm-bindings nil)
+
+(defun llk-init ()
+  (interactive)
+  (define-key special-event-map [low-level-key] 'llk-handle)
+  (define-key special-event-map [low-level-modifier] 'llm-handle)
+
+  (setq llk-bindings nil)
+  (setq llm-bindings nil)
+
+  ;; (llm-bind 'tap 'shift 'delete-other-windows)
+  ;; (llk-bind 'tap 'lctrl 'hyper)
+  (setq enable-low-level-key-events t))
+
+;; For example:
+;; (llk-add-binding 'tap 'lshift 'delete-other-windows)
+;; Can bind to a command, a function or the symbol 'hyper.
+(defun llk-bind (action key function)
+  (push (list action key function) llk-bindings))
+
+(defun llm-bind (action key function)
+  (push (list action key function) llm-bindings))
+
+;; We store the last events here to test for multitap.
+(defvar llk-events nil)
+(defvar llm-events nil)
+
+;; If positive, return key ('lshift, etc) else return nil.
+(defun llk-detect-n-tap (n timeout)
+  ;; The physical-key event is like this:
+  ;; (physical-key t lshift 90196265 #<frame>)
+  ;; The second element is t for a key press, nil for a key release
+  ;; The fourth element is the time in milliseconds
+  ;; The fifth is the frame, we don't use it yet.
+
+  (let ((key (cl-third last-input-event)))
+    (if (not (member key llk-tap-keys))
+        ;; 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 (cl-third (car llk-events)) key))
+           (setq llk-events nil))
+      (push last-input-event 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-fourth (cl-first llk-events))
+                 (cl-fourth (car (last llk-events))))
+              timeout)
+           (progn
+             (setq llk-events nil)
+             key)))))
+
+
+;; this function is a copy of llk-detect-n-tap, but for llm-events
+(defun llm-detect-n-tap (n timeout)
+  (let ((key (cl-third last-input-event)))
+    (if (not (member key llk-tap-keys))
+        (setq llm-events nil)
+      (and llm-events
+           (not (equal (cl-third (car llm-events)) key))
+           (setq llm-events nil))
+      (push last-input-event llm-events)
+      (ntake (* 2 n) llm-events)
+      (and (eq (* 2 n) (length llm-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 llm-events))
+           (< (- (cl-fourth (cl-first llm-events))
+                 (cl-fourth (car (last llm-events))))
+              timeout)
+           (progn
+             (setq llm-events nil)
+             key)))))
+
+(defun llk-handle ()
+  (interactive)
+
+  (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))
+         ((eq 'hyper func)
+          (message "H-...")
+          (let ((r (read-event)))
+            (setq unread-command-events
+                  (list (event-apply-modifier
+                         r 'hyper 24 "H-"))))))))))
+
+(defun llm-handle()
+  (interactive)
+
+  (let ((tap-key (llm-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)))
+                    llm-bindings))))
+        (cond
+         ((commandp func) (call-interactively func))
+         ((functionp func) (funcall func))
+         ((eq 'hyper func)
+          (message "H-...")
+          (let ((r (read-event)))
+            (setq unread-command-events
+                  (list (event-apply-modifier
+                         r 'hyper 24 "H-"))))))))))
diff --git a/src/gtkutil.c b/src/gtkutil.c
index d57627f152f..6ca4e313929 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,105 @@ xg_im_context_preedit_end (GtkIMContext *imc, gpointer user_data)
   kbd_buffer_store_event (&inev);
 }
 
+static void
+xg_maybe_send_low_level_key_event (struct frame *f,
+				   GdkEvent *xev)
+{
+  GdkEventKey xkey = xev->key;
+  bool is_press;
+  int keysym;
+  Lisp_Object key, modifier;
+  union buffered_input_event inev;
+
+  if (!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;
+    }
+
+  keysym = xkey.keyval;
+
+  switch (keysym)
+    {
+    case GDK_KEY_Shift_L: key = Qlshift; break;
+    case GDK_KEY_Shift_R: key = Qrshift; break;
+    case GDK_KEY_Control_L: key = Qlctrl; break;
+    case GDK_KEY_Control_R: key = Qrctrl; break;
+    case GDK_KEY_Alt_L: key = Qlalt; break;
+    case GDK_KEY_Alt_R: key = Qralt; break;
+    default:
+      key = Qnil;
+    }
+
+   switch (keysym)
+    {
+    case GDK_KEY_Shift_L:
+    case GDK_KEY_Shift_R:
+      modifier = Qshift;
+      break;
+    case GDK_KEY_Control_L:
+    case GDK_KEY_Control_R:
+      modifier = Vx_ctrl_keysym;
+      if (NILP (modifier))
+	modifier = Qctrl;
+      break;
+    case GDK_KEY_Alt_L:
+    case GDK_KEY_Alt_R:
+      modifier = Vx_meta_keysym;
+      if (NILP (modifier))
+	modifier = Qalt;
+      break;
+    case GDK_KEY_Meta_L:
+    case GDK_KEY_Meta_R:
+      modifier = Vx_meta_keysym;
+      if (NILP (modifier))
+	modifier = Qmeta;
+      break;
+    case GDK_KEY_Hyper_L:
+    case GDK_KEY_Hyper_R:
+      modifier = Vx_hyper_keysym;
+      if (NILP (modifier))
+	modifier = Qhyper;
+      break;
+    case GDK_KEY_Super_L:
+    case GDK_KEY_Super_R:
+      modifier = Vx_super_keysym;
+      if (NILP (modifier))
+	modifier = Qsuper;
+      break;
+    default:
+      modifier = Qnil;
+    }
+
+  if (!NILP (key))
+    {
+      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 = list2 (is_press ? Qt : Qnil, key);
+      kbd_buffer_store_buffered_event (&inev, &xg_pending_quit_event);
+    }
+
+  if (!NILP (modifier))
+    {
+      EVENT_INIT (inev.ie);
+      XSETFRAME (inev.ie.frame_or_window, f);
+      inev.ie.kind = LOW_LEVEL_MODIFIER_KEY_EVENT;
+      inev.ie.timestamp = xkey.time;
+      inev.ie.arg = list2 (is_press ? Qt : Qnil, modifier);
+      kbd_buffer_store_buffered_event (&inev, &xg_pending_quit_event);
+    }
+}
+
 static bool
 xg_widget_key_press_event_cb (GtkWidget *widget, GdkEvent *event,
 			      gpointer user_data)
@@ -6404,6 +6510,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 +6667,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..532a1d5dbfe 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -4274,6 +4274,8 @@ kbd_buffer_get_event (KBOARD **kbp,
       case CONFIG_CHANGED_EVENT:
       case FOCUS_OUT_EVENT:
       case SELECT_WINDOW_EVENT:
+      case LOW_LEVEL_KEY_EVENT:
+      case LOW_LEVEL_MODIFIER_KEY_EVENT:
         {
           obj = make_lispy_event (&event->ie);
           kbd_fetch_ptr = next_kbd_event (event);
@@ -7118,6 +7120,22 @@ make_lispy_event (struct input_event *event)
     case PREEDIT_TEXT_EVENT:
       return list2 (Qpreedit_text, event->arg);
 
+    case LOW_LEVEL_KEY_EVENT:
+      return listn (5,
+		    Qlow_level_key,
+		    XCAR (event->arg), /* Press or release.  */
+		    XCAR (XCDR (event->arg)), /* The key symbol.  */
+		    make_fixnum (event->timestamp),
+		    event->frame_or_window);
+
+    case LOW_LEVEL_MODIFIER_KEY_EVENT:
+      return listn (5,
+		    Qlow_level_modifier,
+		    XCAR (event->arg), /* Press or release.  */
+		    XCAR (XCDR (event->arg)), /* The key symbol.  */
+		    make_fixnum (event->timestamp),
+		    event->frame_or_window);
+
       /* The 'kind' field of the event is something we don't recognize.  */
     default:
       emacs_abort ();
@@ -12931,6 +12949,20 @@ syms_of_keyboard (void)
   DEFSYM (Qfile_notify, "file-notify");
 #endif /* USE_FILE_NOTIFY */
 
+  DEFVAR_LISP ("enable-low-level-key-events", Venable_low_level_key_events,
+	       doc: /* Enabled the recepcion of low level key events.
+This includes 'low-level-key' and 'low-level-modifier' events.  */);
+  Venable_low_level_key_events = false;
+
+  DEFSYM (Qlow_level_key, "low-level-key");
+  DEFSYM (Qlow_level_modifier, "low-level-modifier");
+  DEFSYM (Qlshift, "lshift");
+  DEFSYM (Qrshift, "rshift");
+  DEFSYM (Qlctrl, "lctrl");
+  DEFSYM (Qrctrl, "rctrl");
+  DEFSYM (Qlalt, "lalt");
+  DEFSYM (Qralt, "ralt");
+
   DEFSYM (Qtouch_end, "touch-end");
 
   /* Menu and tool bar item parts.  */
@@ -14018,6 +14050,12 @@ 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");
+  initial_define_lispy_key (Vspecial_event_map, "low-level-modifier",
+			    "ignore");
+
+
 }
 
 /* Mark the pointers in the kboard objects.
diff --git a/src/pgtkterm.c b/src/pgtkterm.c
index 079945126e0..7091e9ef4c3 100644
--- a/src/pgtkterm.c
+++ b/src/pgtkterm.c
@@ -5201,6 +5201,39 @@ pgtk_enqueue_preedit (struct frame *f, Lisp_Object preedit)
   evq_enqueue (&inev);
 }
 
+static void
+pgtk_maybe_send_low_level_key_event (GdkEvent *event)
+{
+  if (!Venable_low_level_key_events)
+    return;
+
+  Lisp_Object key;
+  switch (event->key.keyval)
+    {
+    case GDK_KEY_Shift_L: key = Qlshift; break;
+    case GDK_KEY_Shift_R: key = Qrshift; break;
+    case GDK_KEY_Control_L: key = Qlctrl; break;
+    case GDK_KEY_Control_R: key = Qrctrl; break;
+    case GDK_KEY_Alt_L: key = Qlalt; break;
+    case GDK_KEY_Alt_R: key = Qralt; break;
+    default:
+      return;
+    }
+  bool keypress = event->key.type == GDK_KEY_PRESS;
+  struct frame *f = pgtk_any_window_to_frame (event->key.window);
+  if (!f)
+    return;
+
+  union buffered_input_event inev;
+
+  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 = list2 (keypress ? Qt : Qnil, key);
+  evq_enqueue (&inev);
+}
+
 static gboolean
 key_press_event (GtkWidget *widget, GdkEvent *event, gpointer *user_data)
 {
@@ -5210,6 +5243,8 @@ key_press_event (GtkWidget *widget, GdkEvent *event, gpointer *user_data)
   struct frame *f;
   struct pgtk_display_info *dpyinfo;
 
+  pgtk_maybe_send_low_level_key_event(event);
+
   f = pgtk_any_window_to_frame (gtk_widget_get_window (widget));
   EVENT_INIT (inev.ie);
   hlinfo = MOUSE_HL_INFO (f);
@@ -5454,6 +5489,8 @@ key_release_event (GtkWidget *widget,
   GdkDisplay *display;
   struct pgtk_display_info *dpyinfo;
 
+  pgtk_maybe_send_low_level_key_event(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..006b74809a3 100644
--- a/src/termhooks.h
+++ b/src/termhooks.h
@@ -347,6 +347,8 @@ #define EMACS_TERMHOOKS_H
   /* In a NOTIFICATION_EVENT, .arg is a lambda to evaluate.  */
   , NOTIFICATION_EVENT
 #endif /* HAVE_ANDROID */
+  , LOW_LEVEL_KEY_EVENT
+  , LOW_LEVEL_MODIFIER_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..f1850a5f2f3 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 (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 (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..9fae9701daf 100644
--- a/src/w32term.c
+++ b/src/w32term.c
@@ -5270,6 +5270,75 @@ 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 = Qnil;
+	      Lisp_Object modifier = Qnil;
+
+	      switch (vk)
+		{
+		case VK_LSHIFT: key = Qlshift; break;
+		case VK_RSHIFT: key = Qrshift; break;
+		case VK_LCONTROL: key = Qlctrl; break;
+		case VK_RCONTROL: key = Qrctrl; break;
+		case VK_LMENU: key = Qlalt; break;
+		case VK_RMENU: key = Qralt; break;
+		}
+
+	      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 (!NILP (key))
+		{
+		  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 = list2 (is_wm_keyup ? Qnil : Qt, key);
+		  kbd_buffer_store_event_hold (&inev, hold_quit);
+
+		}
+
+	      if (!NILP (modifier))
+		{
+		  f = w32_window_to_frame (dpyinfo, msg.msg.hwnd);
+		  inev.kind = LOW_LEVEL_MODIFIER_KEY_EVENT;
+		  XSETFRAME (inev.frame_or_window, f);
+		  inev.timestamp = msg.msg.time;
+		  inev.arg = list2 (is_wm_keyup ? Qnil : Qt, 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..97028c32336 100644
--- a/src/xterm.c
+++ b/src/xterm.c
@@ -17840,6 +17840,143 @@ #define STORE_KEYSYM_FOR_DEBUG(keysym) ((void)0)
 
 static struct x_display_info *next_noop_dpyinfo;
 
+static void
+x_maybe_send_low_level_key_event (struct x_display_info *dpyinfo,
+				  const XEvent *xev)
+{
+  XKeyEvent xkey;
+  bool is_press;
+  KeySym keysym;
+  Lisp_Object key, modifier;
+  struct input_event ie;
+
+  if (!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;
+    }
+
+  struct frame *f = x_any_window_to_frame (dpyinfo, xkey.window);
+  if (!f)
+    return;
+
+  XLookupString (&xkey, NULL, 0, &keysym, NULL);
+
+  switch (keysym)
+    {
+    case XK_Shift_L: key = Qlshift; break;
+    case XK_Shift_R: key = Qrshift; break;
+    case XK_Control_L: key = Qlctrl; break;
+    case XK_Control_R: key = Qrctrl; break;
+    case XK_Alt_L: key = Qlalt; break;
+    case XK_Alt_R: key = Qralt; break;
+    default:
+      key = Qnil;
+    }
+
+   switch (keysym)
+    {
+    case XK_Shift_L:
+    case XK_Shift_R:
+      modifier = Qshift;
+      break;
+    case XK_Control_L:
+    case XK_Control_R:
+      modifier = Vx_ctrl_keysym;
+      if (NILP (modifier))
+	modifier = Qctrl;
+      break;
+    case XK_Alt_L:
+    case XK_Alt_R:
+      modifier = Vx_meta_keysym;
+      if (NILP (modifier))
+	modifier = Qalt;
+      break;
+    case XK_Meta_L:
+    case XK_Meta_R:
+      modifier = Vx_meta_keysym;
+      if (NILP (modifier))
+	modifier = Qmeta;
+      break;
+    case XK_Hyper_L:
+    case XK_Hyper_R:
+      modifier = Vx_hyper_keysym;
+      if (NILP (modifier))
+	modifier = Qhyper;
+      break;
+    case XK_Super_L:
+    case XK_Super_R:
+      modifier = Vx_super_keysym;
+      if (NILP (modifier))
+	modifier = Qsuper;
+      break;
+    default:
+      modifier = Qnil;
+    }
+
+  if (!NILP (key))
+    {
+      EVENT_INIT (ie);
+      XSETFRAME (ie.frame_or_window, f);
+      ie.kind = LOW_LEVEL_KEY_EVENT;
+      ie.timestamp = xkey.time;
+      ie.arg = list2 (is_press ? Qt : Qnil, key);
+      kbd_buffer_store_event (&ie);
+    }
+
+  if (!NILP (modifier))
+    {
+      EVENT_INIT (ie);
+      XSETFRAME (ie.frame_or_window, f);
+      ie.kind = LOW_LEVEL_MODIFIER_KEY_EVENT;
+      ie.timestamp = xkey.time;
+      ie.arg = list2 (is_press ? Qt : Qnil, key);
+      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 +20343,7 @@ handle_one_xevent (struct x_display_info *dpyinfo,
       goto OTHER;
 
     case KeyPress:
+      x_maybe_send_low_level_key_event (dpyinfo, event);
       x_display_set_last_user_time (dpyinfo, event->xkey.time,
 				    event->xkey.send_event,
 				    true);
@@ -20715,6 +20853,7 @@ handle_one_xevent (struct x_display_info *dpyinfo,
 #endif
 
     case KeyRelease:
+      x_maybe_send_low_level_key_event (dpyinfo, event);
 #ifdef HAVE_X_I18N
       /* Don't dispatch this event since XtDispatchEvent calls
          XFilterEvent, and two calls in a row may freeze the
@@ -23970,6 +24109,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);
+
 	      coding = Qlatin_1;
 
 	      /* The code under this label is quite desultory.  There
@@ -24586,6 +24727,8 @@ handle_one_xevent (struct x_display_info *dpyinfo,
 #endif
 
 	  case XI_KeyRelease:
+	    x_maybe_send_low_level_key_event (dpyinfo, event);
+
 #if defined HAVE_X_I18N || defined USE_GTK || defined USE_LUCID
 	    {
 	      XKeyPressedEvent xkey;
@@ -32662,6 +32805,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));
-- 
2.35.1.windows.2