Fix vterm copy mode and test build issues

ober

00f32ab4586b1c7e2dd6dafa048ac2c1d69cccd3

diff --git a/Makefile b/Makefile
index c8121df..8678ba0 100644
--- a/Makefile
+++ b/Makefile
@@ -122,14 +122,16 @@ endif
 NATIVE_LIB := $(JERBOA)/lib/libjerboa_native.$(SHLIB_EXT)
 SSL_SHIM := $(JERBOA_SSL_ROOT)/jerboa_ssl_shim.$(SHLIB_EXT)
 SCINTILLA_SHIM := ./jerboa_scintilla_shim.$(SHLIB_EXT)
-PRELOAD_ENV := $(PRELOAD_VAR)=./libqt_shim.$(SHLIB_EXT):./jerboa_qt_shim.$(SHLIB_EXT):$(NATIVE_LIB):./libjsh-ffi.$(SHLIB_EXT):./libjsh_coreutils_stub.$(SHLIB_EXT):$(SSL_SHIM)
-CORE_PRELOAD_ENV := $(PRELOAD_VAR)=$(NATIVE_LIB):$(SCINTILLA_SHIM):./libjsh-ffi.$(SHLIB_EXT):./libjsh_coreutils_stub.$(SHLIB_EXT):$(SSL_SHIM)
+TLS_STUB := ./jemacs_tls_stubs.$(SHLIB_EXT)
+PRELOAD_ENV := $(PRELOAD_VAR)=./libqt_shim.$(SHLIB_EXT):./jerboa_qt_shim.$(SHLIB_EXT):$(NATIVE_LIB):$(TLS_STUB):./libjsh-ffi.$(SHLIB_EXT):./libjsh_coreutils_stub.$(SHLIB_EXT):$(SSL_SHIM)
+CORE_PRELOAD_ENV := $(PRELOAD_VAR)=$(NATIVE_LIB):$(TLS_STUB):$(SCINTILLA_SHIM):./libjsh-ffi.$(SHLIB_EXT):./libjsh_coreutils_stub.$(SHLIB_EXT):$(SSL_SHIM)
 # -----------------------------------------------------------------------------
 
 export JERBOA_SCINTILLA_LIB := $(CURDIR)
 export JERBOA_PCRE2_LIB := $(JERBOA_PCRE2_ROOT)
 export JSH_FFI_LIB := $(JSH_ROOT)
 export JERBOA_SSL_DIR := $(JERBOA_SSL_ROOT)
+export JEMACS_TLS_STUB_DIR := $(CURDIR)
 export JERBOA_QT_LIB := .
 export JERBOA_QT_SHIM_DIR := .
 ifeq ($(UNAME_S),Darwin)
@@ -492,6 +494,9 @@ libjsh-ffi.$(SHLIB_EXT): $(JSH_ROOT)/ffi-shim.c
 libjsh_coreutils_stub.$(SHLIB_EXT): support/jsh_coreutils_stubs.c
 	gcc $(SHLIB_FLAGS) -O2 -o libjsh_coreutils_stub.$(SHLIB_EXT) support/jsh_coreutils_stubs.c -Wall
 
+jemacs_tls_stubs.$(SHLIB_EXT): support/jemacs_tls_stubs.c
+	gcc $(SHLIB_FLAGS) -O2 -o jemacs_tls_stubs.$(SHLIB_EXT) support/jemacs_tls_stubs.c -Wall
+
 $(NATIVE_LIB):
 	$(MAKE) -C $(JERBOA) native
 
@@ -499,17 +504,17 @@ $(SSL_SHIM): $(JERBOA_SSL_ROOT)/jerboa_ssl_shim.c
 	$(MAKE) -C $(JERBOA_SSL_ROOT) JERBOA_HOME=$(JERBOA) SCHEME=$(SCHEME) JERBUILD=$(VENDORED_JERBUILD)
 
 # macOS native binary — compile all modules + link jemacs-qt binary
-macos: build libqt_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) repl_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT)
+macos: build libqt_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) repl_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT)
 	$(MAKE) -C $(JERBOA_PCRE2_ROOT) SCHEME=$(SCHEME)
 	JSH_DIR=$(JSH) $(PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --script build-binary-qt.ss
 	./jemacs-qt --version
 
-run-qt: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT)
+run-qt: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT)
 	$(PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --script qt-main.ss
 
 # Headless Qt with automation REPL (for Claude).  Uses xvfb-run for
 # virtual display on Linux (static binary needs xcb).  Auto-assigned REPL port.
-run-qt-test: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT)
+run-qt-test: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT)
 	@rm -f $(HOME)/.jerboa-repl-port
 	@LOG="$${TMPDIR:-/tmp}/jemacs-qt-test.log"; \
 	  rm -f "$$LOG"; \
@@ -596,11 +601,11 @@ test-command-registry:
 test-tier3:
 	$(SCHEME) $(LIBDIRS) --script tests/test-tier3.ss
 
-test-tier4:
-	$(SCHEME) $(LIBDIRS) --program tests/test-tier4.ss
+test-tier4: jerboa_scintilla_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+	$(CORE_PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --program tests/test-tier4.ss
 
-test-tier5:
-	$(SCHEME) $(LIBDIRS) --program tests/test-tier5.ss
+test-tier5: jerboa_scintilla_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+	$(CORE_PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --program tests/test-tier5.ss
 
 test-org: test-org-parse test-org-clock test-org-table test-org-agenda \
           test-org-babel test-org-capture test-org-export test-org-list \
@@ -664,13 +669,13 @@ test-org-src:
 test-org-tempo:
 	$(SCHEME) $(LIBDIRS) --script tests/test-org-tempo.ss
 
-test-vtscreen:
-	$(SCHEME) $(LIBDIRS) --script tests/test-vtscreen.ss
+test-vtscreen: jerboa_scintilla_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+	$(CORE_PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --script tests/test-vtscreen.ss
 
 test-debug-repl:
 	$(SCHEME) $(LIBDIRS) --program tests/test-debug-repl.ss
 
-test-qt: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+test-qt: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
 	QT_QPA_PLATFORM=offscreen $(PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --script tests/test-qt.ss
 	@if [ -f tests/test-qt-part2.ss ]; then \
 	  QT_QPA_PLATFORM=offscreen $(PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --script tests/test-qt-part2.ss; \
@@ -682,17 +687,17 @@ test-qt: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB
 test-qt-e2e:
 	bash tests/test-qt-functional.sh ./jemacs-qt
 
-test-emacs:
-	$(SCHEME) $(LIBDIRS) --program tests/test-emacs.ss
+test-emacs: jerboa_scintilla_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+	$(CORE_PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --program tests/test-emacs.ss
 
-test-functional: build jerboa_scintilla_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+test-functional: build jerboa_scintilla_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
 	$(CORE_PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --program tests/test-functional.ss
 
 test-pty:
 	$(SCHEME) $(LIBDIRS) --script tests/test-pty.ss
 
-test-term-hang:
-	$(SCHEME) $(LIBDIRS) --program tests/test-term-hang.ss
+test-term-hang: jerboa_scintilla_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+	$(CORE_PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --program tests/test-term-hang.ss
 
 binary-qt: build
 	$(SCHEME) $(LIBDIRS) --script build-binary-qt.ss
@@ -1688,38 +1693,38 @@ stress-test:
 	$(SCHEME) $(LIBDIRS) --script tests/stress-test.ss --port $(STRESS_PORT) --profile $(STRESS_PROFILE)
 
 # Focused all-in-one stress profiles.
-stress-window: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+stress-window: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
 	$(STRESS_ENV) scripts/run-stress-profile.sh window interpreted
 
 stress-window-static:
 	$(STRESS_ENV) scripts/run-stress-profile.sh window static
 
-stress-buffer: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+stress-buffer: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
 	$(STRESS_ENV) scripts/run-stress-profile.sh buffer interpreted
 
 stress-buffer-static:
 	$(STRESS_ENV) scripts/run-stress-profile.sh buffer static
 
-stress-edit: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+stress-edit: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
 	$(STRESS_ENV) scripts/run-stress-profile.sh edit interpreted
 
 stress-edit-static:
 	$(STRESS_ENV) scripts/run-stress-profile.sh edit static
 
-stress-file: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+stress-file: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
 	$(STRESS_ENV) scripts/run-stress-profile.sh file interpreted
 
 stress-file-static:
 	$(STRESS_ENV) scripts/run-stress-profile.sh file static
 
-stress-chaos: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+stress-chaos: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
 	$(STRESS_ENV) scripts/run-stress-profile.sh chaos interpreted
 
 stress-chaos-static:
 	$(STRESS_ENV) scripts/run-stress-profile.sh chaos static
 
 # Backward-compatible aliases for the historical all-in-one burn-in.
-stress-burn: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
+stress-burn: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) libjsh-ffi.$(SHLIB_EXT) libjsh_coreutils_stub.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT) $(NATIVE_LIB) $(SSL_SHIM)
 	$(STRESS_ENV) scripts/run-stress-profile.sh chaos interpreted
 
 # Backward-compatible alias for static gdb burn-in.
@@ -1746,7 +1751,7 @@ scenario-test:
 	  $(if $(SCENARIO_NO_SHOTS),--no-screenshots)
 
 # All-in-one: launch interpreted jemacs-qt + run scenario driver
-scenario-burn: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT)
+scenario-burn: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) jerboa_qt_shim.$(SHLIB_EXT) jemacs_tls_stubs.$(SHLIB_EXT)
 	@echo "=== Starting jemacs-qt scenario burn-in ==="
 	@rm -f $(HOME)/.jerboa-repl-port scenario-run.log scenario-failures.log
 	@rm -rf scenario-screenshots
diff --git a/lib/jerboa-emacs/qt/app.sls b/lib/jerboa-emacs/qt/app.sls
index 11b97c8..3cd12d7 100644
--- a/lib/jerboa-emacs/qt/app.sls
+++ b/lib/jerboa-emacs/qt/app.sls
@@ -1050,6 +1050,19 @@
                                    echo-label)]
                                 [*snake-active*
                                  (snake-handle-key! app code mods text)]
+                                [(and (or (= code QT_KEY_RETURN)
+                                          (= code QT_KEY_ENTER))
+                                      (zero?
+                                        (bitwise-and mods QT_MOD_CTRL))
+                                      (zero? (bitwise-and mods QT_MOD_ALT))
+                                      (terminal-copy-mode-active?
+                                        (qt-current-buffer
+                                          (app-state-frame app))))
+                                 (execute-command! app 'vterm-copy-done)
+                                 (qt-refresh-after-key!
+                                   app
+                                   echo-label
+                                   "vterm-copy-done")]
                                 [else
                                  (letrec ([terminal-pty-intercept? (lambda (code
                                                                             mods
@@ -2016,13 +2029,17 @@
                      (hash-put! *terminal-views-map* buf rest))))
                entries)))))
      (qt-window-set-post-split-focus-fn!
-       (lambda (new-win)
-         (let* ([buf (qt-edit-window-buffer new-win)]
-                [session (and buf (hash-get *terminal-widget-map* buf))])
+       (lambda (win)
+         (let* ([buf (qt-edit-window-buffer win)]
+                [container (qt-edit-window-container win)]
+                [session (and buf (hash-get *terminal-widget-map* buf))]
+                [view (and session
+                           (> (qt-stacked-widget-current-index container)
+                              0)
+                           (terminal-view-for-container buf container))])
            (and session
-                (terminal-view-for-container
-                  buf
-                  (qt-edit-window-container new-win))))))
+                view
+                (begin (qt-terminal-view-focus! view) view)))))
      (qt-startup-trace "key handler setup done")
      (qt-startup-trace "periodic repl-poll begin")
      (schedule-periodic!
diff --git a/lib/jerboa-emacs/qt/commands-config.sls b/lib/jerboa-emacs/qt/commands-config.sls
index 0bd62d9..4d8cec5 100644
--- a/lib/jerboa-emacs/qt/commands-config.sls
+++ b/lib/jerboa-emacs/qt/commands-config.sls
@@ -21,7 +21,8 @@
    *SCI_SETCODEPAGE* *SC_CP_UTF8* qt-insert-prompt!
    terminal-buffer-counter cmd-term cmd-terminal-send
    cmd-term-interrupt cmd-term-send-eof cmd-term-send-tab
-   cmd-multi-vterm *terminal-copy-mode* vterm-native-session
+   cmd-multi-vterm *terminal-copy-mode*
+   terminal-copy-mode-active? vterm-native-session
    cmd-vterm-copy-mode cmd-vterm-copy-done get-terminal-buffers
    qt-switch-to-terminal! cmd-term-list cmd-term-next
    cmd-term-prev cmd-ediff-files cmd-comment-dwim)
@@ -1174,6 +1175,9 @@
        (cmd-term app))
   (define *terminal-copy-mode*--cell
     (vector (make-hash-table)))
+  (def (terminal-copy-mode-active? buf)
+       "Return true when BUF is currently showing a frozen vterm copy buffer."
+       (and buf (hash-get *terminal-copy-mode* buf)))
   (def (vterm-native-session buf)
        "Return the native QTerminalSession backing BUF, or #f."
        (and buf (hash-get *terminal-widget-map* buf)))
@@ -1195,6 +1199,7 @@
                       [text (qt-terminal-get-screen-text session)])
                   (qt-plain-text-edit-set-read-only! ed #f)
                   (qt-plain-text-edit-set-text! ed text)
+                  (qt-terminal-apply-screen-styles! session ed)
                   (qt-plain-text-edit-set-read-only! ed #t)
                   (qt-stacked-widget-set-current-widget! container ed)
                   (qt-widget-set-focus! ed)
@@ -1202,7 +1207,7 @@
                   (hash-put! *terminal-copy-mode* buf #t)
                   (echo-message!
                     echo
-                    "Copy mode: move/select text, then vterm-copy-mode-done")))]
+                    "Copy mode: select text, RET or vterm-copy-mode-done copies")))]
            [(terminal-buffer? buf)
             (hash-put! *terminal-copy-mode* buf #t)
             (qt-plain-text-edit-set-read-only! ed #t)
diff --git a/lib/jerboa-emacs/qt/commands.sls b/lib/jerboa-emacs/qt/commands.sls
index 87fb04a..c34a10b 100644
--- a/lib/jerboa-emacs/qt/commands.sls
+++ b/lib/jerboa-emacs/qt/commands.sls
@@ -5,35 +5,36 @@
 (library (jerboa-emacs qt commands)
   (export qt-register-all-commands! dired-open-directory!
    qt-open-image-inline! *qt-app-ptr* *terminal-widget-map*
-   *terminal-views-map* terminal-view-for-container
-   terminal-ensure-view! qt-kill-ring-push! *isearch-active*
-   isearch-handle-key! isearch-start-with-text!
-   *qreplace-active* qreplace-handle-key! recent-files-add!
-   recent-files-load! bookmarks-load! session-save!
-   session-restore-files *tab-bar-visible*
-   *auto-revert-tail-buffers* *file-mtimes* file-mtime-record!
-   file-mtime-changed? *eldoc-mode* eldoc-display!
-   *current-theme* *themes* theme-stylesheet load-theme!
-   apply-theme! load-theme define-theme! buffer-touch!
-   custom-keys-load! abbrevs-load! load-init-file!
-   scratch-save! scratch-restore! scratch-update-text!
-   undo-history-record! winner-save! *follow-mode*
-   *view-mode-buffers* *so-long-buffers* check-so-long!
-   savehist-save! savehist-load! gsh-history-save!
-   gsh-history-load! save-place-load! save-place-save!
-   auto-fill-check! *auto-save-disabled-buffers*
-   *require-final-newline* *save-place-enabled*
-   *centered-cursor-mode* uniquify-buffer-name!
-   shell-command-to-string shell-command-to-buffer!
-   register-shell-command! *user-shell-commands*
-   workspace-init! *workspaces* *current-workspace*
-   workspace-add-buffer! workspace-remove-buffer!
-   qt-pulse-line! qt-pulse-tick! qt-pulse-check-jump!
-   qt-idle-highlight-symbol! *diff-hl-active*
-   *qt-describe-key-pending* qt-describe-key-result!
-   *qt-quoted-insert-pending* qt-quoted-insert-handle!
-   qt-record-edit-position! *qt-desktop-save-mode*
-   qt-aggressive-indent-line! *snake-active* snake-handle-key!)
+   *terminal-views-map* terminal-copy-mode-active?
+   terminal-view-for-container terminal-ensure-view!
+   qt-kill-ring-push! *isearch-active* isearch-handle-key!
+   isearch-start-with-text! *qreplace-active*
+   qreplace-handle-key! recent-files-add! recent-files-load!
+   bookmarks-load! session-save! session-restore-files
+   *tab-bar-visible* *auto-revert-tail-buffers* *file-mtimes*
+   file-mtime-record! file-mtime-changed? *eldoc-mode*
+   eldoc-display! *current-theme* *themes* theme-stylesheet
+   load-theme! apply-theme! load-theme define-theme!
+   buffer-touch! custom-keys-load! abbrevs-load!
+   load-init-file! scratch-save! scratch-restore!
+   scratch-update-text! undo-history-record! winner-save!
+   *follow-mode* *view-mode-buffers* *so-long-buffers*
+   check-so-long! savehist-save! savehist-load!
+   gsh-history-save! gsh-history-load! save-place-load!
+   save-place-save! auto-fill-check!
+   *auto-save-disabled-buffers* *require-final-newline*
+   *save-place-enabled* *centered-cursor-mode*
+   uniquify-buffer-name! shell-command-to-string
+   shell-command-to-buffer! register-shell-command!
+   *user-shell-commands* workspace-init! *workspaces*
+   *current-workspace* workspace-add-buffer!
+   workspace-remove-buffer! qt-pulse-line! qt-pulse-tick!
+   qt-pulse-check-jump! qt-idle-highlight-symbol!
+   *diff-hl-active* *qt-describe-key-pending*
+   qt-describe-key-result! *qt-quoted-insert-pending*
+   qt-quoted-insert-handle! qt-record-edit-position!
+   *qt-desktop-save-mode* qt-aggressive-indent-line!
+   *snake-active* snake-handle-key!)
   (import
    (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;-
      getenv path-extension path-absolute? thread? make-mutex
diff --git a/lib/jerboa-emacs/qt/sci-shim.sls b/lib/jerboa-emacs/qt/sci-shim.sls
index 4f53a25..61cb78a 100644
--- a/lib/jerboa-emacs/qt/sci-shim.sls
+++ b/lib/jerboa-emacs/qt/sci-shim.sls
@@ -141,10 +141,10 @@
    qt-terminal-connect-fd! qt-terminal-send-key-event!
    qt-terminal-send-input! qt-terminal-is-running?
    qt-terminal-get-screen-text qt-terminal-interrupt!
-   qt-terminal-set-font! qt-terminal-set-colors!
-   qt-terminal-focus! qt-terminal-widget
-   qt-terminal-view-create qt-terminal-view-destroy!
-   qt-terminal-view-focus!)
+   qt-terminal-apply-screen-styles! qt-terminal-set-font!
+   qt-terminal-set-colors! qt-terminal-focus!
+   qt-terminal-widget qt-terminal-view-create
+   qt-terminal-view-destroy! qt-terminal-view-focus!)
   (import
     (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;-
       getenv path-extension path-absolute? thread? make-mutex
@@ -662,6 +662,13 @@
           (void*)
           string)
          term))
+  (def (qt-terminal-apply-screen-styles! term sci)
+       "Apply the terminal screen's cell colors to SCI after qt-terminal-get-screen-text."
+       ((foreign-procedure "qt_terminal_apply_screen_styles"
+          (void* void*)
+          void)
+         term
+         sci))
   (def (qt-terminal-interrupt! term)
        "Send SIGINT to the terminal's child process."
        ((foreign-procedure "qt_terminal_interrupt" (void*) void)
diff --git a/lib/jerboa-emacs/qt/window.sls b/lib/jerboa-emacs/qt/window.sls
index f696ce8..de010f7 100644
--- a/lib/jerboa-emacs/qt/window.sls
+++ b/lib/jerboa-emacs/qt/window.sls
@@ -172,6 +172,23 @@
   (def (qt-safe-widget-set-focus! context widget)
        (qt-window-check-handle! context "widget" widget)
        (qt-widget-set-focus! widget))
+  (def (qt-dispose-widget! context widget)
+       (qt-window-check-handle! context "widget" widget)
+       (let ([trash (qt-splitter-create QT_HORIZONTAL)])
+         (qt-safe-widget-hide! context widget)
+         (qt-safe-splitter-add-widget! context trash widget)
+         (qt-safe-widget-hide! context trash)
+         (qt-safe-widget-destroy! context trash)))
+  (def (qt-dispose-window-container! context container)
+       (qt-window-pre-container-destroy! container)
+       (qt-dispose-widget! context container))
+  (def (qt-focus-target-for-edit-window win)
+       (or (and *post-split-focus-fn* (*post-split-focus-fn* win))
+           (qt-edit-window-editor win)))
+  (def (qt-focus-edit-window! context win)
+       (qt-safe-widget-set-focus!
+         context
+         (qt-focus-target-for-edit-window win)))
   (def (qt-safe-widget-set-style-sheet! context widget style)
        (qt-window-check-handle! context "widget" widget)
        (qt-widget-set-style-sheet! widget style))
@@ -754,14 +771,9 @@
                saved-w
                saved-h))
            (when result
-             (let ([override (and *post-split-focus-fn*
-                                  (*post-split-focus-fn*
-                                    (list-ref
-                                      (qt-frame-windows fr)
-                                      (qt-frame-current-idx fr))))])
-               (qt-safe-widget-set-focus!
-                 "qt-frame-do-split!"
-                 (or override result))))
+             (qt-focus-edit-window!
+               "qt-frame-do-split!"
+               (list-ref (qt-frame-windows fr) (qt-frame-current-idx fr))))
            (qt-frame-update-visual-indicators! fr)
            (when main-win
              (qt-safe-widget-set-updates-enabled!
@@ -831,14 +843,14 @@
                    (when (and parent-spl
                               (not (eq? parent-spl
                                         (qt-frame-splitter fr))))
+                     (when container
+                       (qt-window-pre-container-destroy! container))
                      (set! container #f)
-                     (qt-safe-widget-destroy!
+                     (qt-dispose-widget!
                        "qt-frame-delete-window!"
                        parent-spl))))))
            (when container
-             (qt-window-pre-container-destroy! container)
-             (qt-safe-widget-hide! "qt-frame-delete-window!" container)
-             (qt-safe-widget-destroy!
+             (qt-dispose-window-container!
                "qt-frame-delete-window!"
                container))
            (qt-frame-windows-set!
@@ -852,9 +864,7 @@
            (let ([new-win (list-ref
                             (qt-frame-windows fr)
                             (qt-frame-current-idx fr))])
-             (qt-safe-widget-set-focus!
-               "qt-frame-delete-window!"
-               (qt-edit-window-editor new-win)))
+             (qt-focus-edit-window! "qt-frame-delete-window!" new-win))
            (qt-frame-update-visual-indicators! fr)
            (qt-validate-window-tree!
              fr
@@ -880,26 +890,18 @@
                (let ([ed (qt-edit-window-editor win)]
                      [container (qt-edit-window-container win)])
                  (hash-remove! *editor-window-map* ed)
-                 (qt-window-pre-container-destroy! container)
-                 (qt-safe-widget-hide!
-                   "qt-frame-delete-other-windows!"
-                   container)
-                 (qt-safe-widget-destroy!
+                 (qt-dispose-window-container!
                    "qt-frame-delete-other-windows!"
                    container))))
            all-wins)
          (for-each
            (lambda (spl)
-             (qt-safe-widget-destroy!
-               "qt-frame-delete-other-windows!"
-               spl))
+             (qt-dispose-widget! "qt-frame-delete-other-windows!" spl))
            sub-spls)
          (qt-frame-root-set! fr (make-split-leaf cur))
          (qt-frame-windows-set! fr (list cur))
          (qt-frame-current-idx-set! fr 0)
-         (qt-safe-widget-set-focus!
-           "qt-frame-delete-other-windows!"
-           (qt-edit-window-editor cur))
+         (qt-focus-edit-window! "qt-frame-delete-other-windows!" cur)
          (qt-frame-update-visual-indicators! fr)
          (qt-validate-window-tree!
            fr
@@ -933,9 +935,7 @@
          (let ([win (list-ref
                       (qt-frame-windows fr)
                       (qt-frame-current-idx fr))])
-           (qt-safe-widget-set-focus!
-             "qt-frame-other-window!"
-             (qt-edit-window-editor win)))
+           (qt-focus-edit-window! "qt-frame-other-window!" win))
          (qt-frame-update-visual-indicators! fr)
          (qt-validate-window-tree!
            fr
diff --git a/scripts/patch-jerboa-std-compat.sh b/scripts/patch-jerboa-std-compat.sh
index 08d33fe..12a78a2 100644
--- a/scripts/patch-jerboa-std-compat.sh
+++ b/scripts/patch-jerboa-std-compat.sh
@@ -28,5 +28,44 @@ patch_import() {
   fi
 }
 
+patch_tls_rustls_stub_loader() {
+  file="$libdir/std/net/tls-rustls.ss"
+
+  [ -f "$file" ] || return 0
+  if grep -q "jemacs_tls_stubs" "$file"; then
+    return 0
+  fi
+
+  tmp="${file}.tmp.$$"
+  awk '
+    $0 == "  ;; ========== FFI declarations ==========" {
+      print "  ;; Dynamic editor/test runs may not link Rust rustls symbols into"
+      print "  ;; libjerboa_native. Load the local no-op TLS symbol shim before"
+      print "  ;; binding foreign procedures so imports that do not use TLS still work."
+      print "  (def _jemacs-tls-stub-loaded"
+      print "    (let* ([mt (symbol->string (machine-type))]"
+      print "           [ext (if (and (>= (string-length mt) 3)"
+      print "                         (string=? (substring mt (- (string-length mt) 3) (string-length mt)) \"osx\"))"
+      print "                  \"dylib\""
+      print "                  \"so\")]"
+      print "           [dir (or (getenv \"JEMACS_TLS_STUB_DIR\")"
+      print "                    (getenv \"JERBOA_EMACS_SUPPORT\")"
+      print "                    (getenv \"JEMACS_BINARY_DIR\")"
+      print "                    \".\")])"
+      print "      (try (begin (load-shared-object (string-append dir \"/jemacs_tls_stubs.\" ext)) #t)"
+      print "           (catch (e) #f))))"
+      print ""
+    }
+    { print }
+  ' "$file" > "$tmp"
+  mv "$tmp" "$file"
+
+  if ! grep -q "jemacs_tls_stubs" "$file"; then
+    echo "patch-jerboa-std-compat: failed to patch $file" >&2
+    exit 1
+  fi
+}
+
 patch_import "$libdir/std/misc/lru-cache.ss"
 patch_import "$libdir/std/misc/rbtree.ss"
+patch_tls_rustls_stub_loader
diff --git a/src/jerboa-emacs/editor-core.ss b/src/jerboa-emacs/editor-core.ss
index 7a3645f..3447a07 100644
--- a/src/jerboa-emacs/editor-core.ss
+++ b/src/jerboa-emacs/editor-core.ss
@@ -720,6 +720,53 @@
   (let ((pos (editor-get-current-pos ed)))
     (editor-set-selection ed pos pos)))
 
+(def (editor-word-char? ch)
+  (or (char-alphabetic? ch)
+      (char-numeric? ch)
+      (char=? ch #\_)))
+
+(def (editor-word-right-pos text pos)
+  (let ((len (string-length text)))
+    (let skip-word ((i pos))
+      (cond
+        ((>= i len) len)
+        ((editor-word-char? (string-ref text i))
+         (skip-word (+ i 1)))
+        (else
+         (let skip-space ((j i))
+           (cond
+             ((>= j len) len)
+             ((not (editor-word-char? (string-ref text j)))
+              (skip-space (+ j 1)))
+             (else j))))))))
+
+(def (editor-word-left-pos text pos)
+  (let skip-space ((i (max 0 (- pos 1))))
+    (cond
+      ((<= pos 0) 0)
+      ((and (> i 0) (not (editor-word-char? (string-ref text i))))
+       (skip-space (- i 1)))
+      ((not (editor-word-char? (string-ref text i))) 0)
+      (else
+       (let skip-word ((j i))
+         (cond
+           ((<= j 0) 0)
+           ((editor-word-char? (string-ref text (- j 1)))
+            (skip-word (- j 1)))
+           (else j)))))))
+
+(def (editor-move-word-right! ed)
+  (let* ((text (editor-get-text ed))
+         (len (string-length text))
+         (pos (min (editor-get-current-pos ed) len)))
+    (editor-goto-pos ed (editor-word-right-pos text pos))))
+
+(def (editor-move-word-left! ed)
+  (let* ((text (editor-get-text ed))
+         (len (string-length text))
+         (pos (min (editor-get-current-pos ed) len)))
+    (editor-goto-pos ed (editor-word-left-pos text pos))))
+
 (def (cmd-forward-char app)
   (let ((n (get-prefix-arg app)) (ed (current-editor app)))
     (collapse-selection-to-caret! ed)
@@ -832,26 +879,30 @@
 
 (def (cmd-forward-word app)
   (let ((n (get-prefix-arg app)) (ed (current-editor app)))
+    (collapse-selection-to-caret! ed)
     (if (>= n 0)
-      (let loop ((i 0)) (when (< i n) (editor-send-key ed SCK_RIGHT ctrl: #t) (loop (+ i 1))))
-      (let loop ((i 0)) (when (< i (- n)) (editor-send-key ed SCK_LEFT ctrl: #t) (loop (+ i 1)))))
+      (let loop ((i 0)) (when (< i n) (editor-move-word-right! ed) (loop (+ i 1))))
+      (let loop ((i 0)) (when (< i (- n)) (editor-move-word-left! ed) (loop (+ i 1)))))
     (update-mark-region! app ed)))
 
 (def (cmd-backward-word app)
   (let ((n (get-prefix-arg app)) (ed (current-editor app)))
+    (collapse-selection-to-caret! ed)
     (if (>= n 0)
-      (let loop ((i 0)) (when (< i n) (editor-send-key ed SCK_LEFT ctrl: #t) (loop (+ i 1))))
-      (let loop ((i 0)) (when (< i (- n)) (editor-send-key ed SCK_RIGHT ctrl: #t) (loop (+ i 1)))))
+      (let loop ((i 0)) (when (< i n) (editor-move-word-left! ed) (loop (+ i 1))))
+      (let loop ((i 0)) (when (< i (- n)) (editor-move-word-right! ed) (loop (+ i 1)))))
     (update-mark-region! app ed)))
 
 (def (cmd-beginning-of-buffer app)
   (let ((ed (current-editor app)))
-    (editor-send-key ed SCK_HOME ctrl: #t)
+    (collapse-selection-to-caret! ed)
+    (editor-goto-pos ed 0)
     (update-mark-region! app ed)))
 
 (def (cmd-end-of-buffer app)
   (let ((ed (current-editor app)))
-    (editor-send-key ed SCK_END ctrl: #t)
+    (collapse-selection-to-caret! ed)
+    (editor-goto-pos ed (send-message ed SCI_GETLENGTH 0 0))
     (update-mark-region! app ed)))
 
 (def (cmd-scroll-down app)
diff --git a/src/jerboa-emacs/editor-extra-regs2.ss b/src/jerboa-emacs/editor-extra-regs2.ss
index 2238e70..b35d750 100644
--- a/src/jerboa-emacs/editor-extra-regs2.ss
+++ b/src/jerboa-emacs/editor-extra-regs2.ss
@@ -20,7 +20,7 @@
           cmd-info-reader cmd-project-tree-git cmd-open-line)
         (only-in :jerboa-emacs/editor-text
           cmd-delete-blank-lines cmd-call-last-kbd-macro cmd-name-last-kbd-macro)
-        (only-in :jerboa-emacs/editor-ui cmd-eval-expression)
+        (only-in :jerboa-emacs/editor-ui cmd-eval-expression cmd-keyboard-quit)
         (only-in :jerboa-emacs/editor-cmds-c cmd-imenu cmd-electric-newline-and-indent)
         (only-in :jerboa-emacs/helm-commands
           cmd-helm-mark-ring cmd-helm-M-x cmd-helm-mini
@@ -108,7 +108,7 @@
                    cmd-sudo-edit cmd-tabify cmd-untabify cmd-view-lossage cmd-what-page
                    cmd-copy-line-number cmd-delete-frame cmd-describe-mode
                    cmd-goto-line-relative cmd-insert-lorem-ipsum cmd-make-frame
-                   cmd-narrow-to-defun cmd-suspend-frame cmd-write-region)
+                   cmd-keyboard-quit cmd-narrow-to-defun cmd-suspend-frame cmd-write-region)
         (except-in :jerboa-emacs/editor-extra-regs
                    cmd-count-lines-page cmd-delete-directory
                    cmd-how-many cmd-iedit-mode cmd-other-frame)
diff --git a/src/jerboa-emacs/qt/app.ss b/src/jerboa-emacs/qt/app.ss
index 04f955b..b837c42 100644
--- a/src/jerboa-emacs/qt/app.ss
+++ b/src/jerboa-emacs/qt/app.ss
@@ -1113,6 +1113,13 @@
                   (qt-echo-draw! (app-state-echo app) echo-label))
                  (*snake-active*
                   (snake-handle-key! app code mods text))
+                 ((and (or (= code QT_KEY_RETURN) (= code QT_KEY_ENTER))
+                       (zero? (bitwise-and mods QT_MOD_CTRL))
+                       (zero? (bitwise-and mods QT_MOD_ALT))
+                       (terminal-copy-mode-active?
+                         (qt-current-buffer (app-state-frame app))))
+                  (execute-command! app 'vterm-copy-done)
+                  (qt-refresh-after-key! app echo-label "vterm-copy-done"))
                  (else
                 ;; Normal key processing — with chord detection
                 (letrec
@@ -1617,17 +1624,23 @@
                       (hash-put! *terminal-views-map* buf rest))))
                 entries)))))
 
-        ;; Install post-split-focus hook: after a split, if the new window shows
-        ;; a terminal, focus its freshly-created QTerminalView rather than the
-        ;; hidden editor page behind it. Returns #f for non-terminal buffers so
-        ;; the splitter falls back to focusing the new editor.
+        ;; Install window-focus hook: when a selected window shows a live
+        ;; terminal page, focus that QTerminalView rather than the hidden editor
+        ;; page behind it. Returns #f for non-terminal buffers and copy-mode
+        ;; terminal buffers so the window manager focuses the editor.
         (qt-window-set-post-split-focus-fn!
-          (lambda (new-win)
-            (let* ((buf (qt-edit-window-buffer new-win))
-                   (session (and buf (hash-get *terminal-widget-map* buf))))
+          (lambda (win)
+            (let* ((buf (qt-edit-window-buffer win))
+                   (container (qt-edit-window-container win))
+                   (session (and buf (hash-get *terminal-widget-map* buf)))
+                   (view (and session
+                              (> (qt-stacked-widget-current-index container) 0)
+                              (terminal-view-for-container buf container))))
               (and session
-                   (terminal-view-for-container
-                     buf (qt-edit-window-container new-win))))))
+                   view
+                   (begin
+                     (qt-terminal-view-focus! view)
+                     view)))))
       (qt-startup-trace "key handler setup done")
 
       ;; ================================================================
diff --git a/src/jerboa-emacs/qt/commands-config.ss b/src/jerboa-emacs/qt/commands-config.ss
index aa7d0f9..02acc03 100644
--- a/src/jerboa-emacs/qt/commands-config.ss
+++ b/src/jerboa-emacs/qt/commands-config.ss
@@ -998,6 +998,10 @@ modified so the next save uses the new encoding."
 ;; Track which terminal buffers are in copy mode
 (def *terminal-copy-mode* (make-hash-table))
 
+(def (terminal-copy-mode-active? buf)
+  "Return true when BUF is currently showing a frozen vterm copy buffer."
+  (and buf (hash-get *terminal-copy-mode* buf)))
+
 (def (vterm-native-session buf)
   "Return the native QTerminalSession backing BUF, or #f."
   (and buf (hash-get *terminal-widget-map* buf)))
@@ -1022,12 +1026,13 @@ modified so the next save uses the new encoding."
                (text (qt-terminal-get-screen-text session)))
            (qt-plain-text-edit-set-read-only! ed #f)   ;; allow the screen dump in
            (qt-plain-text-edit-set-text! ed text)
+           (qt-terminal-apply-screen-styles! session ed)
            (qt-plain-text-edit-set-read-only! ed #t)
            (qt-stacked-widget-set-current-widget! container ed)
            (qt-widget-set-focus! ed)
            (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
            (hash-put! *terminal-copy-mode* buf #t)
-           (echo-message! echo "Copy mode: move/select text, then vterm-copy-mode-done"))))
+           (echo-message! echo "Copy mode: select text, RET or vterm-copy-mode-done copies"))))
       ;; Legacy line-terminal: read-only toggle on the editor (old behavior).
       ((terminal-buffer? buf)
        (hash-put! *terminal-copy-mode* buf #t)
diff --git a/src/jerboa-emacs/qt/commands.ss b/src/jerboa-emacs/qt/commands.ss
index 3d5da5c..8752b70 100644
--- a/src/jerboa-emacs/qt/commands.ss
+++ b/src/jerboa-emacs/qt/commands.ss
@@ -8,6 +8,7 @@
         *qt-app-ptr*
         *terminal-widget-map*
         *terminal-views-map*
+        terminal-copy-mode-active?
         terminal-view-for-container
         terminal-ensure-view!
         qt-kill-ring-push!
diff --git a/src/jerboa-emacs/qt/sci-shim.ss b/src/jerboa-emacs/qt/sci-shim.ss
index 221cb92..9a455b3 100644
--- a/src/jerboa-emacs/qt/sci-shim.ss
+++ b/src/jerboa-emacs/qt/sci-shim.ss
@@ -142,6 +142,7 @@
   qt-terminal-connect-fd!
   qt-terminal-send-key-event! qt-terminal-send-input!
   qt-terminal-is-running? qt-terminal-get-screen-text qt-terminal-interrupt!
+  qt-terminal-apply-screen-styles!
   qt-terminal-set-font! qt-terminal-set-colors!
   qt-terminal-focus! qt-terminal-widget
   qt-terminal-view-create qt-terminal-view-destroy! qt-terminal-view-focus!)
@@ -649,6 +650,10 @@
   "Return the visible terminal screen as plain text (for copy mode)."
   ((foreign-procedure "qt_terminal_get_screen_text" (void*) string) term))
 
+(def (qt-terminal-apply-screen-styles! term sci)
+  "Apply the terminal screen's cell colors to SCI after qt-terminal-get-screen-text."
+  ((foreign-procedure "qt_terminal_apply_screen_styles" (void* void*) void) term sci))
+
 (def (qt-terminal-interrupt! term)
   "Send SIGINT to the terminal's child process."
   ((foreign-procedure "qt_terminal_interrupt" (void*) void) term))
diff --git a/src/jerboa-emacs/qt/window.ss b/src/jerboa-emacs/qt/window.ss
index 3310e9d..d552dc8 100644
--- a/src/jerboa-emacs/qt/window.ss
+++ b/src/jerboa-emacs/qt/window.ss
@@ -43,7 +43,7 @@
         qt-window-set-app-ptr!
         ;; Hook called before any window container is destroyed
         qt-window-set-pre-container-destroy-fn!
-        ;; Hook to override focus target after a split (e.g. terminal views)
+        ;; Hook to override focus target for a selected window (e.g. terminal views)
         qt-window-set-post-split-focus-fn!)
 
 (import :std/sugar
@@ -70,10 +70,10 @@
 (def (qt-window-set-pre-container-destroy-fn! fn)
   (set! *pre-container-destroy-fn* fn))
 
-;; Called with (new-window) after a split to decide which widget gets focus.
-;; Returns a widget to focus, or #f to fall back to the new editor. Registered
-;; by the terminal code so a split that displays a terminal focuses the new
-;; QTerminalView, not the hidden editor page behind it.
+;; Called with (window) when that window should receive focus. Returns a widget
+;; to focus, or #f to fall back to the editor. Registered by the terminal code so
+;; a selected terminal pane focuses the visible QTerminalView, not the hidden
+;; editor page behind it. The historical setter name is kept for callers.
 (def *post-split-focus-fn* #f)
 
 (def (qt-window-set-post-split-focus-fn! fn)
@@ -239,6 +239,31 @@
   (qt-window-check-handle! context "widget" widget)
   (qt-widget-set-focus! widget))
 
+(def (qt-dispose-widget! context widget)
+  (qt-window-check-handle! context "widget" widget)
+  (let ((trash (qt-splitter-create QT_HORIZONTAL)))
+    ;; Moving WIDGET into a detached splitter removes it from its current
+    ;; splitter immediately. qt-widget-destroy! uses deleteLater(), so relying
+    ;; on destruction alone leaves QSplitter counts stale until Qt processes
+    ;; deferred-delete events.
+    (qt-safe-widget-hide! context widget)
+    (qt-safe-splitter-add-widget! context trash widget)
+    (qt-safe-widget-hide! context trash)
+    (qt-safe-widget-destroy! context trash)))
+
+(def (qt-dispose-window-container! context container)
+  (qt-window-pre-container-destroy! container)
+  (qt-dispose-widget! context container))
+
+(def (qt-focus-target-for-edit-window win)
+  (or (and *post-split-focus-fn*
+           (*post-split-focus-fn* win))
+      (qt-edit-window-editor win)))
+
+(def (qt-focus-edit-window! context win)
+  (qt-safe-widget-set-focus! context
+                             (qt-focus-target-for-edit-window win)))
+
 (def (qt-safe-widget-set-style-sheet! context widget style)
   (qt-window-check-handle! context "widget" widget)
   (qt-widget-set-style-sheet! widget style))
@@ -731,11 +756,9 @@
       ;; behind the QTerminalView, so let the registered hook override the target
       ;; to focus the new view instead of the (invisible) editor.
       (when result
-        (let ((override (and *post-split-focus-fn*
-                             (*post-split-focus-fn*
-                               (list-ref (qt-frame-windows fr)
-                                         (qt-frame-current-idx fr))))))
-          (qt-safe-widget-set-focus! "qt-frame-do-split!" (or override result))))
+        (qt-focus-edit-window! "qt-frame-do-split!"
+          (list-ref (qt-frame-windows fr)
+                    (qt-frame-current-idx fr))))
       ;; Update visual indicators
       (qt-frame-update-visual-indicators! fr)
       ;; Re-enable updates — single
@@ -813,25 +836,23 @@
               ;; means destroying parent-spl also destroys container. Track this
               ;; to avoid the double-free in the explicit container destroy below.
               (when (and parent-spl (not (eq? parent-spl (qt-frame-splitter fr))))
+                (when container
+                  (qt-window-pre-container-destroy! container))
                 (set! container #f)   ;; cleared — Qt will destroy it via parent-spl
-                (qt-safe-widget-destroy! "qt-frame-delete-window!"
-                                         parent-spl))))))
+                (qt-dispose-widget! "qt-frame-delete-window!" parent-spl))))))
 
       ;; Destroy the deleted window's Qt container.
       ;; Skipped when container was set to #f above (already destroyed via parent-spl).
       (when container
-        (qt-window-pre-container-destroy! container)
-        (qt-safe-widget-hide! "qt-frame-delete-window!" container)
-        (qt-safe-widget-destroy! "qt-frame-delete-window!" container))
+        (qt-dispose-window-container! "qt-frame-delete-window!" container))
 
       ;; Update flat windows list and current-idx
       (set! (qt-frame-windows fr) (list-remove-idx (qt-frame-windows fr) idx))
       (when (>= (qt-frame-current-idx fr) (length (qt-frame-windows fr)))
         (set! (qt-frame-current-idx fr) (- (length (qt-frame-windows fr)) 1)))
-      ;; Restore focus to the new current editor (destroying widgets may lose it)
+      ;; Restore focus to the new current window (destroying widgets may lose it)
       (let ((new-win (list-ref (qt-frame-windows fr) (qt-frame-current-idx fr))))
-        (qt-safe-widget-set-focus! "qt-frame-delete-window!"
-                                   (qt-edit-window-editor new-win)))
+        (qt-focus-edit-window! "qt-frame-delete-window!" new-win))
       ;; Update visual indicators
       (qt-frame-update-visual-indicators! fr)
       (qt-validate-window-tree! fr "after qt-frame-delete-window!"))))
@@ -852,24 +873,21 @@
                   (let ((ed        (qt-edit-window-editor win))
                         (container (qt-edit-window-container win)))
                     (hash-remove! *editor-window-map* ed)
-                    (qt-window-pre-container-destroy! container)
-                    (qt-safe-widget-hide! "qt-frame-delete-other-windows!"
-                                          container)
-                    (qt-safe-widget-destroy! "qt-frame-delete-other-windows!"
-                                             container))))
+                    (qt-dispose-window-container!
+                      "qt-frame-delete-other-windows!"
+                      container))))
               all-wins)
     ;; 3. Destroy sub-splitters (innermost first, avoiding double-destroy)
     (for-each
       (lambda (spl)
-        (qt-safe-widget-destroy! "qt-frame-delete-other-windows!" spl))
+        (qt-dispose-widget! "qt-frame-delete-other-windows!" spl))
       sub-spls)
     ;; 4. Update logical tree and flat list
     (set! (qt-frame-root fr) (make-split-leaf cur))
     (set! (qt-frame-windows fr) (list cur))
     (set! (qt-frame-current-idx fr) 0)
-    ;; 5. Restore focus to the surviving editor (reparenting may lose it)
-    (qt-safe-widget-set-focus! "qt-frame-delete-other-windows!"
-                               (qt-edit-window-editor cur))
+    ;; 5. Restore focus to the surviving window (reparenting may lose it)
+    (qt-focus-edit-window! "qt-frame-delete-other-windows!" cur)
     ;; 6. Update visual indicators
     (qt-frame-update-visual-indicators! fr)
     (qt-validate-window-tree! fr "after qt-frame-delete-other-windows!")))
@@ -902,10 +920,9 @@
   (let ((n (length (qt-frame-windows fr))))
     (set! (qt-frame-current-idx fr)
           (modulo (+ (qt-frame-current-idx fr) 1) n))
-    ;; Give keyboard focus to the new active editor
+    ;; Give keyboard focus to the new active window.
     (let ((win (list-ref (qt-frame-windows fr) (qt-frame-current-idx fr))))
-      (qt-safe-widget-set-focus! "qt-frame-other-window!"
-                                 (qt-edit-window-editor win)))
+      (qt-focus-edit-window! "qt-frame-other-window!" win))
     ;; Update visual indicators
     (qt-frame-update-visual-indicators! fr)
     (qt-validate-window-tree! fr "after qt-frame-other-window!")))
diff --git a/support/jemacs_tls_stubs.c b/support/jemacs_tls_stubs.c
index cf4c7c9..7ad012b 100644
--- a/support/jemacs_tls_stubs.c
+++ b/support/jemacs_tls_stubs.c
@@ -2,6 +2,23 @@
 
 #include <stddef.h>
 #include <stdint.h>
+#include <string.h>
+
+size_t jerboa_last_error(uint8_t *buf, size_t len) {
+    const char *msg = "TLS not available in jemacs test stub";
+    size_t msg_len = strlen(msg);
+    size_t copy_len = msg_len;
+
+    if (len == 0) {
+        return msg_len;
+    }
+    if (copy_len >= len) {
+        copy_len = len - 1;
+    }
+    memcpy(buf, msg, copy_len);
+    buf[copy_len] = 0;
+    return copy_len;
+}
 
 uint64_t jerboa_tls_server_new(const char *cert, const char *key) {
     (void)cert;
diff --git a/support/vendor-overrides/qt_shim.cpp b/support/vendor-overrides/qt_shim.cpp
index 776ad1a..5ab9285 100644
--- a/support/vendor-overrides/qt_shim.cpp
+++ b/support/vendor-overrides/qt_shim.cpp
@@ -8164,6 +8164,36 @@ QWidget* QTerminalSession::representativeWidget() {
 typedef void* qt_terminal_t;        // QTerminalSession*
 typedef void* qt_terminal_view_t;   // QTerminalView* (a QWidget*)
 
+static long term_color_to_sci(const QColor& c) {
+    return (long)(c.red() | (c.green() << 8) | (c.blue() << 16));
+}
+
+static QColor term_resolve_vterm_color(QTerminalSession* s,
+                                       VTermScreen* screen,
+                                       const VTermColor& color,
+                                       bool foreground) {
+    if ((foreground && VTERM_COLOR_IS_DEFAULT_FG(&color)) ||
+        (!foreground && VTERM_COLOR_IS_DEFAULT_BG(&color))) {
+        return foreground ? s->m_default_fg : s->m_default_bg;
+    }
+
+    VTermColor c = color;
+    if (VTERM_COLOR_IS_INDEXED(&c))
+        vterm_screen_convert_color_to_rgb(screen, &c);
+    return QColor(c.rgb.red, c.rgb.green, c.rgb.blue);
+}
+
+struct TerminalStyleCell {
+    int byte_start;
+    int byte_len;
+    long fg;
+    long bg;
+    bool bold;
+    bool italic;
+    bool underline;
+    bool space;
+};
+
 // Create a terminal SESSION (model only — no view yet). The parent argument is
 // ignored (the session is a parentless QObject); kept for ABI compatibility.
 extern "C" qt_terminal_t qt_terminal_create(qt_widget_t parent) {
@@ -8302,6 +8332,121 @@ extern "C" const char* qt_terminal_get_screen_text(qt_terminal_t term) {
     })());
 }
 
+extern "C" void qt_terminal_apply_screen_styles(qt_terminal_t term,
+                                                qt_scintilla_t sci) {
+    QT_NULL_CHECK_VOID(term);
+    QT_NULL_CHECK_VOID(sci);
+    QT_VOID(
+        auto* session = static_cast<QTerminalSession*>(term);
+        auto* editor = static_cast<QsciScintilla*>(sci);
+        VTermScreen* screen = session->m_screen;
+        if (screen) {
+            const int first_style = 80;
+            const int max_style = 249;
+            int next_style = first_style;
+            std::unordered_map<unsigned long long, int> style_map;
+
+            auto style_for = [&](long fg, long bg, bool bold,
+                                 bool italic, bool underline) -> int {
+                unsigned long long key =
+                    ((unsigned long long)(fg & 0xFFFFFF) << 28) |
+                    ((unsigned long long)(bg & 0xFFFFFF) << 4) |
+                    (bold ? 1ULL : 0ULL) |
+                    (italic ? 2ULL : 0ULL) |
+                    (underline ? 4ULL : 0ULL);
+                auto it = style_map.find(key);
+                if (it != style_map.end()) return it->second;
+                if (next_style > max_style) return QsciScintillaBase::STYLE_DEFAULT;
+
+                int style = next_style++;
+                style_map[key] = style;
+                editor->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE,
+                                      style, fg);
+                editor->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK,
+                                      style, bg);
+                editor->SendScintilla(QsciScintillaBase::SCI_STYLESETBOLD,
+                                      style, bold ? 1L : 0L);
+                editor->SendScintilla(QsciScintillaBase::SCI_STYLESETITALIC,
+                                      style, italic ? 1L : 0L);
+                editor->SendScintilla(QsciScintillaBase::SCI_STYLESETUNDERLINE,
+                                      style, underline ? 1L : 0L);
+                return style;
+            };
+
+            std::vector<std::vector<TerminalStyleCell>> rows;
+            rows.reserve(session->m_rows);
+            int last_nonempty_row = -1;
+
+            for (int row = 0; row < session->m_rows; ++row) {
+                std::vector<TerminalStyleCell> cells;
+                int byte_pos = 0;
+                for (int col = 0; col < session->m_cols; ++col) {
+                    VTermPos pos = { row, col };
+                    VTermScreenCell cell;
+                    vterm_screen_get_cell(screen, pos, &cell);
+
+                    QString text;
+                    bool space = false;
+                    if (cell.chars[0] != 0) {
+                        char32_t cp = static_cast<char32_t>(cell.chars[0]);
+                        text = QString::fromUcs4(&cp, 1);
+                    } else {
+                        text = QLatin1Char(' ');
+                        space = true;
+                    }
+
+                    QByteArray utf8 = text.toUtf8();
+                    int utf8_len = static_cast<int>(utf8.size());
+                    QColor fg = term_resolve_vterm_color(session, screen, cell.fg, true);
+                    QColor bg = term_resolve_vterm_color(session, screen, cell.bg, false);
+                    if (cell.attrs.reverse) std::swap(fg, bg);