Add Qt mouse press handling

ober

c16c41920e5952c922350a907574d4783eb0555c

diff --git a/Makefile b/Makefile
index ecd01be..129f550 100644
--- a/Makefile
+++ b/Makefile
@@ -300,6 +300,8 @@ vendor-deps:
 	@sh scripts/patch-jerboa-std-compat.sh "$(abspath $(JERBOA)/lib)"
 	@sh scripts/patch-jerboa-shell-compat.sh "$(CURDIR)"
 	@scripts/patch-jerboa-aws-compat.sh "$(CURDIR)"
+	@echo "=== Transpiling vendored jerboa-qt ==="
+	@$(MAKE) -C "$(CQT_ROOT)" JERBUILD="$(abspath $(VENDORED_JERBUILD))" transpile
 	@sh scripts/patch-jerboa-qt-compat.sh "$(CURDIR)"
 	@echo "=== Vendor complete ==="
 
diff --git a/dependencies.lock b/dependencies.lock
index 49b7c9d..b9f2a92 100644
--- a/dependencies.lock
+++ b/dependencies.lock
@@ -4,7 +4,7 @@ JERBOA_REV := aecb3f873c58569f19ec62d8ac34901c49c496f9
 JERBOA_SHELL_REV := a861227afb4a6e343a358b5bfec1700989ee479d
 JERBOA_PCRE2_REV := 00ed6cba49c8b6a477da575e8acd1eea993c911e
 JERBOA_SCINTILLA_REV := 96745c5ea69c95f1c4fa2ce80f43e9cdf59a55a6
-JERBOA_QT_REV := dc86e8354ed3406066e9ce7482c0d1d43e4c6272
+JERBOA_QT_REV := cc28fd393b82625fcbe5af26866ff2461c5e6ccd
 JERBOA_SSL_REV := f466a3149f6c9a0e6f8dd202e00bb746a9bacd2c
 JERBOA_HTTPS_REV := 966c2f83e746e0153c7a843b235d8a2e466e1560
 JERBOA_SQLITE_REV := 51d9ca3f186094ab492ac98e4a07637947e51091
diff --git a/scripts/patch-jerboa-qt-compat.sh b/scripts/patch-jerboa-qt-compat.sh
index 270545b..e13dbf9 100755
--- a/scripts/patch-jerboa-qt-compat.sh
+++ b/scripts/patch-jerboa-qt-compat.sh
@@ -15,8 +15,8 @@
 # string-split/string-join from the prelude when vendored qt.sls carries local
 # helper definitions for those names. Idempotent.
 #
-# Patches the transpiled .sls (the build input — jerboa-qt's lib/ is used as-is,
-# not re-transpiled). Run from vendor-deps.
+# Patches the transpiled .sls generated by vendor-deps from the locked
+# jerboa-qt source revision.
 set -eu
 
 repo="${1:-.}"
diff --git a/src/jerboa-emacs/qt/app.ss b/src/jerboa-emacs/qt/app.ss
index 992cebb..25a5e84 100644
--- a/src/jerboa-emacs/qt/app.ss
+++ b/src/jerboa-emacs/qt/app.ss
@@ -839,6 +839,7 @@
 (def *qt-edit-version-counter* 0)
 (def *qt-buffer-edit-versions* (make-hash-table))
 (def *qt-text-change-listener-editors* (make-hash-table))
+(def *qt-mouse-listener-editors* (make-hash-table))
 
 (def (qt-buffer-edit-version buf)
   "Return BUF's monotonic edit version for debounce checks."
@@ -859,6 +860,29 @@
     (qt-on-plain-text-edit-text-changed! editor
       (lambda ()
         (qt-bump-edit-version! editor)))))
+(def (qt-install-mouse-listener! app editor)
+  "Install one mouse-press callback on EDITOR for point/modeline refresh."
+  (when (and editor (not (hash-get *qt-mouse-listener-editors* editor)))
+    (hash-put! *qt-mouse-listener-editors* editor #t)
+    (qt-on-mouse-press! editor
+      (lambda ()
+        (with-key-handler-errors app
+          (when (= (qt-last-mouse-button) QT_MOUSE_LEFT)
+            (let* ((fr (app-state-frame app))
+                   (widget (qt-last-mouse-widget))
+                   (x (qt-last-mouse-x))
+                   (y (qt-last-mouse-y))
+                   (pos (sci-send editor SCI_POSITIONFROMPOINT x y)))
+              (let loop ((wins (qt-frame-windows fr)) (idx 0))
+                (cond
+                  ((null? wins) (void))
+                  ((eq? editor (qt-edit-window-editor (car wins)))
+                   (set! (qt-frame-current-idx fr) idx))
+                  (else (loop (cdr wins) (+ idx 1)))))
+              (when (and (eq? widget editor) (>= pos 0))
+                (qt-plain-text-edit-set-cursor-position! editor pos))
+              (qt-widget-set-focus! editor)
+              (qt-refresh-after-key! app (app-state-echo app) "mouse-press"))))))))
 (def (qt-refresh-after-key! app echo-label origin)
   (let* ((fr (app-state-frame app))
          (trace? (eq? (buffer-lexer-lang (qt-current-buffer fr)) 'dired)))
@@ -1613,12 +1637,14 @@
 
         ;; Install on the initial editor (consuming — editor doesn't see keys)
         (qt-install-text-change-listener! (qt-current-editor fr))
+        (qt-install-mouse-listener! app (qt-current-editor fr))
         (qt-on-key-press-consuming! (qt-current-editor fr) key-handler)
 
         ;; Store installer so split-window can install on new editors
         (set! (app-state-key-handler app)
               (lambda (editor)
                 (qt-install-text-change-listener! editor)
+                (qt-install-mouse-listener! app editor)
                 (qt-on-key-press-consuming! editor key-handler)))
 
         ;; Tell automation which widget to target for key events. When a terminal
diff --git a/src/jerboa-emacs/qt/sci-shim.ss b/src/jerboa-emacs/qt/sci-shim.ss
index b3a33dd..b942b78 100644
--- a/src/jerboa-emacs/qt/sci-shim.ss
+++ b/src/jerboa-emacs/qt/sci-shim.ss
@@ -73,8 +73,10 @@
   qt-layout-set-spacing! qt-layout-set-stretch-factor!
   ;; Label
   qt-label-create qt-label-set-alignment! qt-label-set-pixmap! qt-label-set-text! qt-label-text
-  ;; Key events
+  ;; Key and mouse events
   qt-last-key-code qt-last-key-modifiers qt-last-key-text qt-last-key-autorepeat? qt-last-key-widget
+  qt-on-mouse-press! qt-last-mouse-x qt-last-mouse-y qt-last-mouse-button
+  qt-last-mouse-modifiers qt-last-mouse-widget QT_MOUSE_LEFT
   ;; Line edit
   qt-line-edit-create qt-line-edit-set-completer! qt-line-edit-set-text! qt-line-edit-text
   ;; List widget
@@ -197,6 +199,9 @@
              QT_CURSOR_PREVIOUS_CHAR QT_CURSOR_PREVIOUS_WORD)
   :std/sugar)
 
+(def (qt-app-process-events! app)
+  (qt-pump-events! app))
+
 ;; SCI_REPLACESEL constant
 
 ;; Qt modifier/key/cursor constants re-exported for modules that only import sci-shim.
@@ -263,6 +268,8 @@
 (def (qt-stacked-widget-set-current-widget! sw widget)
   (ffi-qt-stacked-widget-set-current-widget sw widget))
 
+(def QT_MOUSE_LEFT 1)
+
 (def ffi-qt-last-key-widget
   (foreign-procedure "qt_last_key_widget" () void*))
 
diff --git a/support/vendor-overrides/qt_chez_shim.c b/support/vendor-overrides/qt_chez_shim.c
index f0b21fa..92be38b 100644
--- a/support/vendor-overrides/qt_chez_shim.c
+++ b/support/vendor-overrides/qt_chez_shim.c
@@ -21,6 +21,9 @@
 #include <stdatomic.h>
 #include <stdio.h>
 
+/* Loader ABI canary: "JQT1". */
+unsigned int jerboa_qt_abi_version(void) { return 0x4a515431u; }
+
 /* ---- Stored callback function pointers (set from Chez at init time) ---- */
 static qt_callback_void   jerboa_void_callback   = NULL;
 static qt_callback_string jerboa_string_callback  = NULL;
@@ -45,6 +48,7 @@ static struct cb_entry cb_queue[CB_QUEUE_SIZE];
 static _Atomic unsigned int cb_head = 0;  /* written by producer (Qt thread) */
 static _Atomic unsigned int cb_tail = 0;  /* written by consumer (primordial) */
 static _Atomic unsigned int cb_dropped = 0;
+static struct cb_entry cb_dispatch;
 
 static void cb_log_drop_to_file(unsigned int n) {
     const char *home = getenv("HOME");
@@ -139,6 +143,76 @@ static void deferred_string_callback(long id, const char* s) { cb_push_string(id
 static void deferred_int_callback(long id, int v)         { cb_push_int(id, v); }
 static void deferred_bool_callback(long id, int v)        { cb_push_bool(id, v); }
 
+/* ---- Polling API expected by current jerboa-qt ---- */
+int jerboa_qt_next_callback_event(void) {
+    unsigned int t = atomic_load_explicit(&cb_tail, memory_order_relaxed);
+    unsigned int h = atomic_load_explicit(&cb_head, memory_order_acquire);
+    int type = 0;
+
+    memset(&cb_dispatch, 0, sizeof(cb_dispatch));
+    if (t == h) {
+        return 0;
+    }
+
+    cb_dispatch = cb_queue[t];
+    t = (t + 1) & (CB_QUEUE_SIZE - 1);
+    atomic_store_explicit(&cb_tail, t, memory_order_release);
+
+    switch (cb_dispatch.type) {
+    case CB_VOID:   type = 1; break;
+    case CB_STRING: type = 2; break;
+    case CB_INT:    type = 3; break;
+    case CB_BOOL:   type = 4; break;
+    }
+    return type;
+}
+
+long jerboa_qt_callback_event_id(void) {
+    return cb_dispatch.id;
+}
+
+int jerboa_qt_callback_event_value(void) {
+    return cb_dispatch.int_value;
+}
+
+const char *jerboa_qt_callback_event_string(void) {
+    return cb_dispatch.str_value;
+}
+
+unsigned long jerboa_qt_callback_pending_count(void) {
+    unsigned int h = atomic_load_explicit(&cb_head, memory_order_acquire);
+    unsigned int t = atomic_load_explicit(&cb_tail, memory_order_relaxed);
+    return (unsigned long)((h - t) & (CB_QUEUE_SIZE - 1));
+}
+
+unsigned long jerboa_qt_callback_dropped_count(void) {
+    return (unsigned long)atomic_load_explicit(&cb_dropped, memory_order_relaxed);
+}
+
+void jerboa_qt_clear_callback_events(void) {
+    atomic_store_explicit(&cb_head, 0, memory_order_release);
+    atomic_store_explicit(&cb_tail, 0, memory_order_release);
+    atomic_store_explicit(&cb_dropped, 0, memory_order_relaxed);
+    memset(&cb_dispatch, 0, sizeof(cb_dispatch));
+}
+
+void jerboa_qt_watch_destroyed(void* object, long callback_id) {
+    (void)object;
+    (void)callback_id;
+}
+
+void jerboa_qt_unwatch_destroyed(void* object, long callback_id) {
+    (void)object;
+    (void)callback_id;
+}
+
+void jerboa_qt_clear_destroyed_watches(void) {
+}
+
+void jerboa_qt_notify_destroying_tree(void* object) {
+    (void)object;
+}
+
 /* ---- Drain function: called from primordial Chez thread ---- */
 void jerboa_qt_drain_pending_callbacks(void) {
     unsigned int t = atomic_load_explicit(&cb_tail, memory_order_relaxed);
@@ -177,6 +251,22 @@ void* jerboa_qt_application_create(void) {
     return qt_application_create(0, NULL);
 }
 
+/* NULL-safe string getters for current jerboa-qt FFI names. */
+const char *jerboa_qt_label_text(void *label) {
+    const char *s = qt_label_text(label);
+    return s ? s : "";
+}
+
+const char *jerboa_qt_push_button_text(void *button) {
+    const char *s = qt_push_button_text(button);
+    return s ? s : "";
+}
+
+const char *jerboa_qt_line_edit_text(void *edit) {
+    const char *s = qt_line_edit_text(edit);
+    return s ? s : "";
+}
+
 /* ---- Signal connection wrappers ---- */
 /* Each wrapper inserts the deferred callback as the trampoline.
  * The Qt thread only writes to the ring buffer — never calls Scheme. */
@@ -302,6 +392,10 @@ void jerboa_qt_install_key_handler_consuming(void* w, long callback_id) {
     if (jerboa_void_callback)
         qt_widget_install_key_handler_consuming(w, deferred_void_callback, callback_id);
 }
+void jerboa_qt_install_mouse_handler(void* w, long callback_id) {
+    if (jerboa_void_callback)
+        qt_widget_install_mouse_handler(w, deferred_void_callback, callback_id);
+}
 
 /* Radio Button */
 void jerboa_qt_radio_button_on_toggled(void* r, long callback_id) {
diff --git a/support/vendor-overrides/qt_shim.cpp b/support/vendor-overrides/qt_shim.cpp
index 181ebd4..31a9fe1 100644
--- a/support/vendor-overrides/qt_shim.cpp
+++ b/support/vendor-overrides/qt_shim.cpp
@@ -40,6 +40,7 @@
 #include <QSplitter>
 #include <QEvent>
 #include <QKeyEvent>
+#include <QMouseEvent>
 #include <QPixmap>
 #include <QIcon>
 #include <QRadioButton>
@@ -582,6 +583,11 @@ static int s_last_key_modifiers = 0;
 static std::string s_last_key_text;
 static int s_last_key_autorepeat = 0;
 static QObject* s_last_key_widget = nullptr;  // which widget fired the last key event
+static int s_last_mouse_x = 0;
+static int s_last_mouse_y = 0;
+static int s_last_mouse_button = 0;
+static int s_last_mouse_modifiers = 0;
+static QObject* s_last_mouse_widget = nullptr;
 
 // Storage for QInputDialog ok/cancel flag
 static bool s_last_input_ok = false;
@@ -643,6 +649,29 @@ private:
     long m_callback_id;
 };
 
+class MousePressFilter : public QObject {
+public:
+    MousePressFilter(QObject* parent, qt_callback_void callback, long callback_id)
+        : QObject(parent), m_callback(callback), m_callback_id(callback_id) {}
+
+    bool eventFilter(QObject* obj, QEvent* event) override {
+        if (event->type() == QEvent::MouseButtonPress) {
+            auto* me = static_cast<QMouseEvent*>(event);
+            s_last_mouse_x = me->position().toPoint().x();
+            s_last_mouse_y = me->position().toPoint().y();
+            s_last_mouse_button = static_cast<int>(me->button());
+            s_last_mouse_modifiers = static_cast<int>(me->modifiers());
+            s_last_mouse_widget = obj;
+            m_callback(m_callback_id);
+        }
+        return QObject::eventFilter(obj, event);
+    }
+
+private:
+    qt_callback_void m_callback;
+    long m_callback_id;
+};
+
 // ============================================================
 // Application lifecycle
 // ============================================================
@@ -2487,6 +2516,17 @@ extern "C" void qt_widget_install_key_handler_consuming(qt_widget_t w,
     );
 }
 
+extern "C" void qt_widget_install_mouse_handler(qt_widget_t w,
+                                                 qt_callback_void callback,
+                                                 long callback_id) {
+    QT_NULL_CHECK_VOID(w);
+    QT_VOID(
+        auto* widget = static_cast<QWidget*>(w);
+        auto* filter = new MousePressFilter(widget, callback, callback_id);
+        widget->installEventFilter(filter)
+    );
+}
+
 extern "C" int qt_last_key_code(void) {
     QT_RETURN(int, s_last_key_code);
 }
@@ -2509,6 +2549,26 @@ extern "C" void* qt_last_key_widget(void) {
     return s_last_key_widget;
 }
 
+extern "C" int qt_last_mouse_x(void) {
+    QT_RETURN(int, s_last_mouse_x);
+}
+
+extern "C" int qt_last_mouse_y(void) {
+    QT_RETURN(int, s_last_mouse_y);
+}
+
+extern "C" int qt_last_mouse_button(void) {
+    QT_RETURN(int, s_last_mouse_button);
+}
+
+extern "C" int qt_last_mouse_modifiers(void) {
+    QT_RETURN(int, s_last_mouse_modifiers);
+}
+
+extern "C" void* qt_last_mouse_widget(void) {
+    return s_last_mouse_widget;
+}
+
 extern "C" void qt_send_key_event(qt_widget_t w, int type, int key, int modifiers, const char* text) {
     QT_NULL_CHECK_VOID(w);
     // Capture primitives by value; construct QKeyEvent inside the lambda
diff --git a/support/vendor-overrides/qt_shim.h b/support/vendor-overrides/qt_shim.h
index c90cddf..e575d4f 100644
--- a/support/vendor-overrides/qt_shim.h
+++ b/support/vendor-overrides/qt_shim.h
@@ -351,11 +351,19 @@ void qt_widget_install_key_handler(qt_widget_t w,
 void qt_widget_install_key_handler_consuming(qt_widget_t w,
                                               qt_callback_void callback,
                                               long callback_id);
+void qt_widget_install_mouse_handler(qt_widget_t w,
+                                     qt_callback_void callback,
+                                     long callback_id);
 int         qt_last_key_code(void);
 int         qt_last_key_modifiers(void);
 const char* qt_last_key_text(void);
 int         qt_last_key_autorepeat(void);
 void*       qt_last_key_widget(void);
+int         qt_last_mouse_x(void);
+int         qt_last_mouse_y(void);
+int         qt_last_mouse_button(void);
+int         qt_last_mouse_modifiers(void);
+void*       qt_last_mouse_widget(void);
 void        qt_send_key_event(qt_widget_t w, int type, int key, int modifiers, const char* text);
 
 /* ========== Phase 7: Images, Icons, Radio Buttons, GroupBox ========== */
diff --git a/tests/test-qt-part2.ss b/tests/test-qt-part2.ss
index df425dd..cb3188f 100644
--- a/tests/test-qt-part2.ss
+++ b/tests/test-qt-part2.ss
@@ -93,11 +93,13 @@
 
 (display "\n=== Qt Part2 Groups 44-55 ===\n")
 
-(test-case "group44 qt key fidelity"
+(test-case "group44 qt key/mouse fidelity"
   (check (qt-key-event->string QT_KEY_RETURN 0 "") => "C-m")
   (check (qt-key-event->string QT_KEY_RETURN QT_MOD_CTRL "") => "C-<return>")
   (check (qt-key-event->string QT_KEY_SPACE QT_MOD_CTRL "") => "C-@")
-  (check (qt-key-event->string QT_KEY_BACKSPACE 0 "") => "DEL"))
+  (check (qt-key-event->string QT_KEY_BACKSPACE 0 "") => "DEL")
+  (check QT_MOUSE_LEFT => 1)
+  (check qt-on-mouse-press! ? procedure?))
 
 (test-case "group45 command registry shard smoke"
   (check (find-command 'scroll-up) ? procedure?)