Share Qt vterm sessions across split windows
ober
c3d067292a010b0d2f0f53c7e986deefccc8a371
--- a/lib/jerboa-emacs/qt/app.sls +++ b/lib/jerboa-emacs/qt/app.sls @@ -10,11 +10,11 @@ mutex? mutex-name) (std sugar) (std misc string) (jerboa-scintilla constants) (jerboa-emacs qt sci-shim) - (except (jerboa-qt qt) QT_MOD_NONE QT_MOD_SHIFT QT_MOD_CONTROL - QT_MOD_ALT QT_MOD_META QT_KEY_ESCAPE QT_KEY_BACKSPACE - QT_KEY_RETURN QT_KEY_ENTER QT_KEY_DELETE QT_KEY_TAB - QT_KEY_BACKTAB QT_KEY_INSERT QT_KEY_HOME QT_KEY_END - QT_KEY_LEFT QT_KEY_RIGHT QT_KEY_UP QT_KEY_DOWN + (except (jerboa-qt qt) QT_MOD_NONE QT_MOD_SHIFT + QT_MOD_CONTROL QT_MOD_ALT QT_MOD_META QT_KEY_ESCAPE + QT_KEY_BACKSPACE QT_KEY_RETURN QT_KEY_ENTER QT_KEY_DELETE + QT_KEY_TAB QT_KEY_BACKTAB QT_KEY_INSERT QT_KEY_HOME + QT_KEY_END QT_KEY_LEFT QT_KEY_RIGHT QT_KEY_UP QT_KEY_DOWN QT_KEY_PAGE_UP QT_KEY_PAGE_DOWN QT_KEY_SPACE QT_KEY_F1 QT_KEY_F2 QT_KEY_F3 QT_KEY_F4 QT_KEY_F5 QT_KEY_F6 QT_KEY_F7 QT_KEY_F8 QT_KEY_F9 QT_KEY_F10 QT_KEY_F11 QT_KEY_F12 @@ -775,8 +775,7 @@ "commands registered: " (number->string (hash-length *all-commands*)) " total") - (let ([image-key-installed (make-hash-table-eq)] - [terminal-key-installed (make-hash-table-eq)]) + (let ([image-key-installed (make-hash-table-eq)]) (add-hook! 'post-buffer-attach-hook (lambda (editor buf) @@ -789,24 +788,23 @@ (lambda () (cond [(hash-get *terminal-widget-map* buf) => - (lambda (term) + (lambda (session) (let ([win (hash-get *editor-window-map* editor)]) (when win (let* ([container (qt-edit-window-container win)] - [count (qt-stacked-widget-count - container)] - [tw (qt-terminal-widget term)]) - (unless (hash-get terminal-key-installed tw) - ((app-state-key-handler app) tw) - (hash-put! terminal-key-installed tw #t)) - (if (> count 1) - (begin - (qt-stacked-widget-set-current-widget! - container - tw) - (qt-widget-set-focus! tw)) - (qt-widget-set-focus! editor))))))] + [view (terminal-ensure-view! + app + buf + session + container)]) + (qt-stacked-widget-set-current-widget! + container + view) + (when (eq? win + (qt-current-window + (app-state-frame app))) + (qt-terminal-view-focus! view))))))] [(image-buffer? buf) (qt-show-image-buffer! editor buf) (let ([win (hash-get *editor-window-map* editor)]) @@ -1579,10 +1577,21 @@ qt-buf))] [key-src-widget (qt-last-key-widget)] [key-from-terminal? (and qt-term - (equal? - key-src-widget - (qt-terminal-widget - qt-term)))]) + (let loop ([views (or (hash-get + *terminal-views-map* + qt-buf) + '())]) + (cond + [(null? + views) + #f] + [(equal? + key-src-widget + (cdr (car views))) + #t] + [else + (loop + (cdr views))])))]) (if (and qt-term key-from-terminal? (null? @@ -1839,30 +1848,52 @@ (qt-on-key-press-consuming! editor key-handler))) (automation-set-key-target-fn! (lambda (fr) - (let* ([buf (qt-current-buffer fr)] - [term (and buf - (hash-get *terminal-widget-map* buf))]) - (if term - (qt-terminal-widget term) - (qt-current-editor fr))))) + (let* ([win (qt-current-window fr)] + [buf (qt-edit-window-buffer win)] + [view (and buf + (hash-get *terminal-widget-map* buf) + (terminal-view-for-container + buf + (qt-edit-window-container win)))]) + (or view (qt-current-editor fr))))) (qt-window-set-pre-container-destroy-fn! (lambda (container) - (let ([bufs-to-remove '()]) + (let ([entries '()]) (hash-for-each - (lambda (buf stored-container) - (when (equal? stored-container container) - (let ([term (hash-get *terminal-widget-map* buf)]) - (when term - (with-catch - (lambda (e) #f) - (lambda () (qt-terminal-destroy! term))))) - (set! bufs-to-remove (cons buf bufs-to-remove)))) - *terminal-container-map*) + (lambda (buf views) + (set! entries (cons (cons buf views) entries))) + *terminal-views-map*) (for-each - (lambda (buf) - (hash-remove! *terminal-widget-map* buf) - (hash-remove! *terminal-container-map* buf)) - bufs-to-remove))))) + (lambda (e) + (let* ([buf (car e)] + [views (cdr e)] + [mine (filter + (lambda (cv) + (equal? (car cv) container)) + views)] + [rest (filter + (lambda (cv) + (not (equal? (car cv) container))) + views)]) + (unless (null? mine) + (for-each + (lambda (cv) + (with-catch + (lambda (e) #f) + (lambda () + (qt-terminal-view-destroy! (cdr cv))))) + mine) + (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))]) + (and session + (terminal-view-for-container + buf + (qt-edit-window-container new-win)))))) (schedule-periodic! 'repl-poll 50 @@ -2577,19 +2608,16 @@ (let* ([fr (app-state-frame app)] [win (qt-current-window fr)] [buf (qt-edit-window-buffer win)] - [term (and buf - (hash-get - *terminal-widget-map* - buf))]) - (and term - (let* ([container (qt-edit-window-container - win)] - [count (qt-stacked-widget-count - container)]) - (and (> count 1) - (> (qt-stacked-widget-current-index - container) - 0))))))) + [session (and buf + (hash-get + *terminal-widget-map* + buf))] + [container (qt-edit-window-container win)]) + (and session + (terminal-view-for-container buf container) + (> (qt-stacked-widget-current-index container) + 0) + #t)))) (cons 'test-reset! (lambda () @@ -2597,17 +2625,15 @@ (let ([term-bufs (hash-keys *terminal-widget-map*)]) (for-each (lambda (buf) - (let ([term (hash-get - *terminal-widget-map* - buf)]) - (when term + (let ([session (hash-get + *terminal-widget-map* + buf)]) + (when session (with-catch (lambda (e) #f) - (lambda () (qt-terminal-destroy! term))) + (lambda () (qt-terminal-destroy! session))) (hash-remove! *terminal-widget-map* buf) - (hash-remove! - *terminal-container-map* - buf)))) + (hash-remove! *terminal-views-map* buf)))) term-bufs)) (when (> (length (qt-frame-windows (app-state-frame app))) --- a/lib/jerboa-emacs/qt/commands-config.sls +++ b/lib/jerboa-emacs/qt/commands-config.sls @@ -899,8 +899,8 @@ (loop (cdr segs) (+ pos text-len))))))) (define terminal-buffer-counter--cell (vector 0)) (def (cmd-term app) - "Open a QTerminalWidget-backed terminal running jsh as the shell." - (verbose-log! "cmd-term: begin (QTerminalWidget + jsh)") + "Open a QTerminalSession-backed terminal running jsh as the shell.\n The session is shared by all windows showing the buffer; each window gets\n its own QTerminalView, so splitting the window shows the same live terminal." + (verbose-log! "cmd-term: begin (QTerminalSession + jsh)") (let* ([fr (app-state-frame app)] [ed (current-qt-editor app)] [name (begin @@ -927,24 +927,22 @@ (lambda () (let* ([win (qt-current-window fr)] [container (qt-edit-window-container win)] - [term (qt-terminal-create container)] + [session (qt-terminal-create container)] [jsh-path (or (getenv "JSH") "/usr/local/bin/jsh")]) (qt-terminal-set-font! - term + session *default-font-family* *default-font-size*) - (qt-terminal-set-colors! term 12305103 2632756) - (qt-stacked-widget-add-widget! - container - (qt-terminal-widget term)) - (qt-stacked-widget-set-current-widget! - container - (qt-terminal-widget term)) - (qt-terminal-spawn! term jsh-path) - (hash-put! *terminal-widget-map* buf term) - (hash-put! *terminal-container-map* buf container) - ((app-state-key-handler app) (qt-terminal-widget term)) - (qt-terminal-focus! term) + (qt-terminal-set-colors! session 12305103 2632756) + (hash-put! *terminal-widget-map* buf session) + (let ([view (terminal-ensure-view! + app + buf + session + container)]) + (qt-stacked-widget-set-current-widget! container view) + (qt-terminal-view-focus! view) + (qt-terminal-spawn! session jsh-path)) (verbose-log! "cmd-term: spawned jsh=" jsh-path) (echo-message! (app-state-echo app) --- a/lib/jerboa-emacs/qt/commands-shell.sls +++ b/lib/jerboa-emacs/qt/commands-shell.sls @@ -4,7 +4,8 @@ (library (jerboa-emacs qt commands-shell) (export directory-exists? *terminal-widget-map* - *terminal-container-map* apply-font-size-to-all-editors! + *terminal-views-map* terminal-view-for-container + terminal-ensure-view! apply-font-size-to-all-editors! cmd-increase-font-size cmd-decrease-font-size cmd-reset-font-size cmd-goto-first-non-blank cmd-goto-last-non-blank cmd-move-to-window-top @@ -118,8 +119,27 @@ (and (file-exists? path) (file-directory? path))) (define *terminal-widget-map*--cell (vector (make-hash-table-eq))) - (define *terminal-container-map*--cell + (define *terminal-views-map*--cell (vector (make-hash-table-eq))) + (def (terminal-view-for-container buf container) + "Return the QTerminalView showing terminal BUF inside CONTAINER, or #f.\n Containers are foreign pointers, so compare with equal? (via assoc)." + (let ([views (hash-get *terminal-views-map* buf)]) + (cond + [(and views (assoc container views)) => cdr] + [else #f]))) + (def (terminal-ensure-view! app buf session container) + "Get or create the QTerminalView showing SESSION inside CONTAINER. Adds it as\n a QStackedWidget page and installs the consuming key filter exactly once.\n Returns the view." + (or (terminal-view-for-container buf container) + (let ([view (qt-terminal-view-create session container)]) + (qt-stacked-widget-add-widget! container view) + (let ([kh (app-state-key-handler app)]) (when kh (kh view))) + (hash-put! + *terminal-views-map* + buf + (cons + (cons container view) + (or (hash-get *terminal-views-map* buf) '()))) + view))) (def (apply-font-size-to-all-editors! app) "Apply the current global font size to all open editors." (let ([fr (app-state-frame app)] @@ -2280,11 +2300,11 @@ *terminal-widget-map*--cell 0 val)])) - (define-syntax *terminal-container-map* + (define-syntax *terminal-views-map* (identifier-syntax - [id (vector-ref *terminal-container-map*--cell 0)] + [id (vector-ref *terminal-views-map*--cell 0)] [(set! id val) (vector-set! - *terminal-container-map*--cell + *terminal-views-map*--cell 0 val)])) (define-syntax *auto-indent* --- a/lib/jerboa-emacs/qt/commands.sls +++ b/lib/jerboa-emacs/qt/commands.sls @@ -5,7 +5,8 @@ (library (jerboa-emacs qt commands) (export qt-register-all-commands! dired-open-directory! qt-open-image-inline! *qt-app-ptr* *terminal-widget-map* - *terminal-container-map* qt-kill-ring-push! *isearch-active* + *terminal-views-map* terminal-view-for-container + terminal-ensure-view! qt-kill-ring-push! *isearch-active* isearch-handle-key! *qreplace-active* qreplace-handle-key! recent-files-add! recent-files-load! bookmarks-load! session-save! session-restore-files *tab-bar-visible* @@ -662,6 +663,15 @@ (when ts (terminal-stop! ts) (hash-remove! *terminal-state* buf))) + (let ([session (hash-get + *terminal-widget-map* + buf)]) + (when session + (with-catch + (lambda (e) #f) + (lambda () (qt-terminal-destroy! session))) + (hash-remove! *terminal-widget-map* buf) + (hash-remove! *terminal-views-map* buf))) (let ([cs (hash-get *chat-state* buf)]) (when cs (chat-stop! cs) @@ -710,6 +720,13 @@ (when ts (terminal-stop! ts) (hash-remove! *terminal-state* buf))) + (let ([session (hash-get *terminal-widget-map* buf)]) + (when session + (with-catch + (lambda (e) #f) + (lambda () (qt-terminal-destroy! session))) + (hash-remove! *terminal-widget-map* buf) + (hash-remove! *terminal-views-map* buf))) (let ([cs (hash-get *chat-state* buf)]) (when cs (chat-stop! cs) (hash-remove! *chat-state* buf))) (set! *buffer-recent* --- a/lib/jerboa-emacs/qt/sci-shim.sls +++ b/lib/jerboa-emacs/qt/sci-shim.sls @@ -142,7 +142,8 @@ qt-terminal-send-input! qt-terminal-is-running? qt-terminal-interrupt! qt-terminal-set-font! qt-terminal-set-colors! qt-terminal-focus! - qt-terminal-widget) + 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 @@ -669,9 +670,24 @@ "Give keyboard focus to the terminal widget." ((foreign-procedure "qt_terminal_focus" (void*) void) term)) (def (qt-terminal-widget term) - "Return the QWidget* pointer for the terminal (for adding to QStackedWidget)." + "Return a representative view (active-or-first) of the terminal SESSION as a\n QWidget* pointer. Compatibility shim — prefer qt-terminal-view-* for views." ((foreign-procedure "qt_terminal_widget" (void*) void*) term)) + (def (qt-terminal-view-create session parent) + "Create a QTerminalView of SESSION parented into PARENT (a QStackedWidget).\n Registers the view with the session; returns the view's QWidget* pointer." + ((foreign-procedure "qt_terminal_view_create" + (void* void*) + void*) + session + parent)) + (def (qt-terminal-view-destroy! view) + "Destroy a single terminal VIEW (detach from its stack, unregister, delete).\n The owning session and its PTY/shell are left running." + ((foreign-procedure "qt_terminal_view_destroy" (void*) void) + view)) + (def (qt-terminal-view-focus! view) + "Give keyboard focus to a specific terminal VIEW." + ((foreign-procedure "qt_terminal_view_focus" (void*) void) + view)) (define-syntax *doc-editor-map* (identifier-syntax [id (vector-ref *doc-editor-map*--cell 0)] --- a/lib/jerboa-emacs/qt/window.sls +++ b/lib/jerboa-emacs/qt/window.sls @@ -29,7 +29,8 @@ qt-apply-editor-theme! split-tree-flatten split-tree-find-parent split-tree-find-leaf split-tree-collect-sub-splitters qt-window-set-app-ptr! - qt-window-set-pre-container-destroy-fn!) + qt-window-set-pre-container-destroy-fn! + qt-window-set-post-split-focus-fn!) (import (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex @@ -44,6 +45,9 @@ (def *pre-container-destroy-fn* #f) (def (qt-window-set-pre-container-destroy-fn! fn) (set! *pre-container-destroy-fn* fn)) + (def *post-split-focus-fn* #f) + (def (qt-window-set-post-split-focus-fn! fn) + (set! *post-split-focus-fn* fn)) (def (qt-window-pre-container-destroy! container) (when *pre-container-destroy-fn* (*pre-container-destroy-fn* container))) @@ -530,6 +534,8 @@ [new-win (make-qt-edit-window new-ed container buf lna #f #f)]) (qt-scintilla-setup-editor! new-ed) + (qt-stacked-widget-add-widget! container new-ed) + (hash-put! *editor-window-map* new-ed new-win) (qt-buffer-attach! new-ed buf) (let loop ([i 0]) (when (<= i 127) @@ -556,7 +562,6 @@ SCI_STYLESETFORE STYLE_LINENUMBER (rgb->sci r g b))))) - (qt-stacked-widget-add-widget! container new-ed) (if insert-index (qt-safe-splitter-insert-widget! "qt-make-new-window!" @@ -567,7 +572,6 @@ "qt-make-new-window!" container-parent container)) - (hash-put! *editor-window-map* new-ed new-win) new-win)) (def (qt-frame-init! main-win splitter) "Create frame with one QScintilla editor in a QStackedWidget in a QSplitter.\n Returns the frame struct." @@ -577,13 +581,13 @@ [lna (qt-line-number-area-create editor)] [win (make-qt-edit-window editor container buf lna #f #f)]) (qt-scintilla-setup-editor! editor) - (qt-buffer-attach! editor buf) (qt-stacked-widget-add-widget! container editor) + (hash-put! *editor-window-map* editor win) + (qt-buffer-attach! editor buf) (qt-safe-splitter-add-widget! "qt-frame-init!" splitter container) - (hash-put! *editor-window-map* editor win) (let* ([root (make-split-leaf win)] [fr (make-qt-frame splitter root (list win) 0 main-win)]) (qt-frame-update-visual-indicators! fr) @@ -597,16 +601,7 @@ [cur-leaf (split-tree-find-leaf (qt-frame-root fr) cur-win)] [parent (split-tree-find-parent (qt-frame-root fr) cur-win)] [cur-buf (qt-edit-window-buffer cur-win)] - [new-buf (if (eq? (buffer-lexer-lang cur-buf) 'terminal) - (or (find - (lambda (b) - (string=? - (buffer-name b) - buffer-scratch-name)) - (map qt-edit-window-buffer - (qt-frame-windows fr))) - cur-buf) - cur-buf)] + [new-buf cur-buf] [main-win (qt-frame-main-win fr)] [saved-w (and main-win (qt-widget-width main-win))] [saved-h (and main-win (qt-widget-height main-win))]) @@ -759,7 +754,14 @@ saved-w saved-h)) (when result - (qt-safe-widget-set-focus! "qt-frame-do-split!" 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-frame-update-visual-indicators! fr) (when main-win (qt-safe-widget-set-updates-enabled! --- a/src/jerboa-emacs/qt/app.ss +++ b/src/jerboa-emacs/qt/app.ss @@ -826,8 +826,7 @@ ;; When showing an image, install key handler on the scroll area and ;; set focus there — the Scintilla editor is hidden by QStackedWidget ;; so it can't receive key events. - (let ((image-key-installed (make-hash-table-eq)) - (terminal-key-installed (make-hash-table-eq))) + (let ((image-key-installed (make-hash-table-eq))) (add-hook! 'post-buffer-attach-hook (lambda (editor buf) (with-catch @@ -836,26 +835,22 @@ (with-output-to-string (lambda () (display-exception e))))) (lambda () (cond - ;; QTerminalWidget buffers: switch stacked to terminal view + ;; Terminal buffers: ensure THIS window has a QTerminalView of + ;; the shared session and show it. Every window displaying the + ;; buffer gets its own live view — Emacs split semantics. The + ;; key filter is installed by terminal-ensure-view! exactly once. ((hash-get *terminal-widget-map* buf) - => (lambda (term) + => (lambda (session) (let ((win (hash-get *editor-window-map* editor))) (when win (let* ((container (qt-edit-window-container win)) - (count (qt-stacked-widget-count container)) - (tw (qt-terminal-widget term))) - ;; Always install consuming key filter (idempotent via guard) - (unless (hash-get terminal-key-installed tw) - ((app-state-key-handler app) tw) - (hash-put! terminal-key-installed tw #t)) - (if (> count 1) - ;; Terminal widget lives in THIS container — show and focus it - (begin - (qt-stacked-widget-set-current-widget! container tw) - (qt-widget-set-focus! tw)) - ;; Terminal widget is in another window (e.g. after C-x 2). - ;; Just show the editor in this window — don't steal focus. - (qt-widget-set-focus! editor))))))) + (view (terminal-ensure-view! app buf session container))) + (qt-stacked-widget-set-current-widget! container view) + ;; Only steal focus when this is the selected window; + ;; background attaches (splits, previews) must not — the + ;; post-split-focus hook focuses the split's new view. + (when (eq? win (qt-current-window (app-state-frame app))) + (qt-terminal-view-focus! view))))))) ;; Image buffers ((image-buffer? buf) (qt-show-image-buffer! editor buf) @@ -1268,16 +1263,20 @@ ;; C-x prefix and M-x pass through to jemacs keymap. ;; ;; FOCUS GUARD: only forward to the terminal if the key event came - ;; FROM the QTerminalWidget itself. If the user clicked a different - ;; window (so the key came from that window's QScintilla), we must - ;; NOT forward to the terminal even if qt-current-buffer is still - ;; a terminal buffer (Chez state may lag the Qt focus change). + ;; FROM one of this terminal's views. If the user clicked a + ;; different window (so the key came from that window's QScintilla), + ;; we must NOT forward even if qt-current-buffer is still a terminal + ;; buffer (Chez state may lag the Qt focus change). A key from ANY + ;; view of the same buffer is valid — every pane feeds one session. (let* ((qt-buf (qt-current-buffer (app-state-frame app))) (qt-term (and qt-buf (hash-get *terminal-widget-map* qt-buf))) (key-src-widget (qt-last-key-widget)) (key-from-terminal? (and qt-term - (equal? key-src-widget - (qt-terminal-widget qt-term))))) + (let loop ((views (or (hash-get *terminal-views-map* qt-buf) '()))) + (cond + ((null? views) #f) + ((equal? key-src-widget (cdr (car views))) #t) + (else (loop (cdr views)))))))) (if (and qt-term key-from-terminal? ;; key must come FROM this terminal widget ;; Not in a prefix key state (e.g. after C-x) @@ -1402,41 +1401,60 @@ (lambda (editor) (qt-on-key-press-consuming! editor key-handler))) - ;; Tell automation which widget to target for key events. - ;; When a terminal buffer is active, the QTerminalWidget is the visible + ;; Tell automation which widget to target for key events. When a terminal + ;; buffer is active, the current window's QTerminalView is the visible ;; focused widget; the QScintilla editor is hidden behind it in the - ;; QStackedWidget. Sending sendEvent to a non-current QStackedWidget - ;; page is unreliable — use the visible QTerminalWidget instead. + ;; QStackedWidget. Target this window's view; fall back to the editor. (automation-set-key-target-fn! (lambda (fr) - (let* ((buf (qt-current-buffer fr)) - (term (and buf (hash-get *terminal-widget-map* buf)))) - (if term - (qt-terminal-widget term) - (qt-current-editor fr))))) - - ;; Install pre-container-destroy hook so that when any window container - ;; (QStackedWidget) is about to be destroyed (by delete-other-windows, - ;; C-x 0, kill-terminal-buffer, etc.), any terminal living inside it - ;; is detached and destroyed first — preventing the double-free crash - ;; that occurs when Qt auto-deletes the terminal as a child. + (let* ((win (qt-current-window fr)) + (buf (qt-edit-window-buffer win)) + (view (and buf + (hash-get *terminal-widget-map* buf) + (terminal-view-for-container + buf (qt-edit-window-container win))))) + (or view (qt-current-editor fr))))) + + ;; Install pre-container-destroy hook: when any window container + ;; (QStackedWidget) is about to be destroyed (delete-other-windows, C-x 0, + ;; kill-buffer-and-window, etc.), destroy only the QTerminalView(s) living + ;; in THAT container — detached first to avoid the Qt parent-child + ;; double-free. The QTerminalSession and its shell keep running, so the + ;; terminal survives in the other windows (Emacs semantics: closing a + ;; window does not kill the process). (qt-window-set-pre-container-destroy-fn! (lambda (container) - (let ((bufs-to-remove '())) + ;; Snapshot entries first, then mutate (never edit a hash mid-iteration). + (let ((entries '())) (hash-for-each - (lambda (buf stored-container) - (when (equal? stored-container container) - (let ((term (hash-get *terminal-widget-map* buf))) - (when term - (with-catch (lambda (e) #f) - (lambda () (qt-terminal-destroy! term))))) - (set! bufs-to-remove (cons buf bufs-to-remove)))) - *terminal-container-map*) + (lambda (buf views) (set! entries (cons (cons buf views) entries))) + *terminal-views-map*) (for-each - (lambda (buf) - (hash-remove! *terminal-widget-map* buf) - (hash-remove! *terminal-container-map* buf)) - bufs-to-remove))))) + (lambda (e) + (let* ((buf (car e)) + (views (cdr e)) + (mine (filter (lambda (cv) (equal? (car cv) container)) views)) + (rest (filter (lambda (cv) (not (equal? (car cv) container))) views))) + (unless (null? mine) + (for-each + (lambda (cv) + (with-catch (lambda (e) #f) + (lambda () (qt-terminal-view-destroy! (cdr cv))))) + mine) + (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. + (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)))) + (and session + (terminal-view-for-container + buf (qt-edit-window-container new-win)))))) ;; ================================================================ ;; Periodic tasks — registered with schedule-periodic!, driven @@ -1975,42 +1993,41 @@ (cons 'test-prefix-active? (lambda () (not (null? (key-state-prefix-keys (app-state-key-state app)))))) - ;; Is the current buffer a QTerminalWidget terminal? + ;; Does the current window actually display a live terminal view? (cons 'test-terminal-running? - ;; True only if the current window's QStackedWidget is actually - ;; showing the QTerminalWidget page (not just the buffer being a - ;; terminal buffer — after C-x 2, new window shows editor page). + ;; True iff the current window has a QTerminalView of the + ;; terminal buffer AND its stack is showing a page > 0 (the + ;; editor is page 0). After C-x 2 each pane has its own view, + ;; so this is true in every window showing the terminal. (lambda () (let* ((fr (app-state-frame app)) (win (qt-current-window fr)) (buf (qt-edit-window-buffer win)) - (term (and buf (hash-get *terminal-widget-map* buf)))) - (and term - (let* ((container (qt-edit-window-container win)) - (count (qt-stacked-widget-count container))) - ;; Terminal widget was added as the last page (index count-1). - ;; If count > 1 and current index > 0, terminal is visible. - (and (> count 1) - (> (qt-stacked-widget-current-index container) 0))))))) + (session (and buf (hash-get *terminal-widget-map* buf))) + (container (qt-edit-window-container win))) + (and session + (terminal-view-for-container buf container) + (> (qt-stacked-widget-current-index container) 0) + #t)))) ;; Reset editor to a clean single-window state between tests. ;; Clears key prefix state, collapses to one window, destroys terminals. (cons 'test-reset! (lambda () ;; Clear any pending prefix key (e.g. C-x) (set! (app-state-key-state app) (make-initial-key-state)) - ;; Destroy terminals BEFORE delete-other-windows: qt-terminal-destroy! - ;; detaches the widget from its parent QStackedWidget, preventing the - ;; double-free that occurs when delete-other-windows destroys the - ;; container and Qt auto-deletes its children. + ;; Destroy terminal SESSIONS before delete-other-windows. + ;; qt-terminal-destroy! tears down every view (detaching each + ;; from its container) and the PTY, preventing the double-free + ;; when delete-other-windows later destroys the containers. (let ((term-bufs (hash-keys *terminal-widget-map*))) (for-each (lambda (buf) - (let ((term (hash-get *terminal-widget-map* buf))) - (when term + (let ((session (hash-get *terminal-widget-map* buf))) + (when session (with-catch (lambda (e) #f) - (lambda () (qt-terminal-destroy! term))) + (lambda () (qt-terminal-destroy! session))) (hash-remove! *terminal-widget-map* buf) - (hash-remove! *terminal-container-map* buf)))) + (hash-remove! *terminal-views-map* buf)))) term-bufs)) ;; Collapse to single window (safe now: terminals detached from containers) (when (> (length (qt-frame-windows (app-state-frame app))) 1) --- a/src/jerboa-emacs/qt/commands-config.ss +++ b/src/jerboa-emacs/qt/commands-config.ss @@ -753,11 +753,14 @@ modified so the next save uses the new encoding." (def terminal-buffer-counter 0) -;; *terminal-widget-map* and *terminal-container-map* are defined in commands-shell +;; *terminal-widget-map*, *terminal-views-map* and terminal-ensure-view! are +;; defined in commands-shell (imported here). (def (cmd-term app) - "Open a QTerminalWidget-backed terminal running jsh as the shell." - (verbose-log! "cmd-term: begin (QTerminalWidget + jsh)") + "Open a QTerminalSession-backed terminal running jsh as the shell. + The session is shared by all windows showing the buffer; each window gets + its own QTerminalView, so splitting the window shows the same live terminal." + (verbose-log! "cmd-term: begin (QTerminalSession + jsh)") (let* ((fr (app-state-frame app)) (ed (current-qt-editor app)) (name (begin @@ -779,17 +782,19 @@ modified so the next save uses the new encoding." (lambda () (let* ((win (qt-current-window fr)) (container (qt-edit-window-container win)) - (term (qt-terminal-create container)) + (session (qt-terminal-create container)) (jsh-path (or (getenv "JSH") "/usr/local/bin/jsh"))) - (qt-terminal-set-font! term *default-font-family* *default-font-size*) - (qt-terminal-set-colors! term #xbbc2cf #x282c34) - (qt-stacked-widget-add-widget! container (qt-terminal-widget term)) - (qt-stacked-widget-set-current-widget! container (qt-terminal-widget term)) - (qt-terminal-spawn! term jsh-path) - (hash-put! *terminal-widget-map* buf term) - (hash-put! *terminal-container-map* buf container) - ((app-state-key-handler app) (qt-terminal-widget term)) - (qt-terminal-focus! term) + (qt-terminal-set-font! session *default-font-family* *default-font-size*) + (qt-terminal-set-colors! session #xbbc2cf #x282c34) + ;; Map the session BEFORE creating the view: the post-buffer-attach + ;; hook may fire re-entrantly and must find the session to attach a view. + (hash-put! *terminal-widget-map* buf session) + (let ((view (terminal-ensure-view! app buf session container))) + (qt-stacked-widget-set-current-widget! container view) + ;; Focus before spawning so the session sizes the PTY from the real + ;; pane before the shell prints its prompt. + (qt-terminal-view-focus! view) + (qt-terminal-spawn! session jsh-path)) (verbose-log! "cmd-term: spawned jsh=" jsh-path) (echo-message! (app-state-echo app) (string-append name " started"))))))) @@ -1175,4 +1180,3 @@ If on blank line: insert comment and indent." (if (>= i line) (+ offset (string-length new-line)) (loop (+ i 1) (+ offset (string-length (list-ref new-lines i)) 1)))))) (qt-plain-text-edit-set-cursor-position! ed pos))))))))) - --- a/src/jerboa-emacs/qt/commands-shell.ss +++ b/src/jerboa-emacs/qt/commands-shell.ss @@ -59,15 +59,39 @@ ;;; Batch 8: Remaining missing commands ;;;============================================================================ -;; Map buffer → QTerminalWidget pointer for the libvterm-based terminal -;; Defined here (commands-shell) because commands-config imports commands-shell, -;; so moving it here makes it visible to both without circular dependency. +;; Map terminal buffer → QTerminalSession pointer (the model: PTY + libvterm). +;; One session per terminal buffer, shared by every window showing it; each +;; window gets its own QTerminalView. Defined here (commands-shell) because +;; commands-config imports commands-shell, so both see it without a circular +;; dependency. (Name kept for compatibility — values are now sessions.) (def *terminal-widget-map* (make-hash-table-eq)) -;; Map buffer → QStackedWidget container for the terminal's host window. -;; Lets the pre-container-destroy hook identify which terminal is in a container -;; that is about to be destroyed, so it can detach/destroy the terminal first. -(def *terminal-container-map* (make-hash-table-eq)) +;; Map terminal buffer → list of (container . view) pairs, one entry per window +;; currently displaying the terminal. CONTAINER is the window's QStackedWidget; +;; VIEW is the QTerminalView page inside it. Lets the pre-container-destroy hook +;; tear down only the view(s) in a dying container while the session lives on. +(def *terminal-views-map* (make-hash-table-eq)) + +(def (terminal-view-for-container buf container) + "Return the QTerminalView showing terminal BUF inside CONTAINER, or #f. + Containers are foreign pointers, so compare with equal? (via assoc)." + (let ((views (hash-get *terminal-views-map* buf))) + (cond ((and views (assoc container views)) => cdr) + (else #f)))) + +(def (terminal-ensure-view! app buf session container) + "Get or create the QTerminalView showing SESSION inside CONTAINER. Adds it as + a QStackedWidget page and installs the consuming key filter exactly once. + Returns the view." + (or (terminal-view-for-container buf container) + (let ((view (qt-terminal-view-create session container))) + (qt-stacked-widget-add-widget! container view) + (let ((kh (app-state-key-handler app))) + (when kh (kh view))) + (hash-put! *terminal-views-map* buf + (cons (cons container view) + (or (hash-get *terminal-views-map* buf) '()))) + view))) ;; --- Font size --- ;; Note: Font size state is now in face.ss (*default-font-size*) --- a/src/jerboa-emacs/qt/commands.ss +++ b/src/jerboa-emacs/qt/commands.ss @@ -7,7 +7,9 @@ qt-open-image-inline! *qt-app-ptr* *terminal-widget-map* - *terminal-container-map* + *terminal-views-map* + terminal-view-for-container + terminal-ensure-view! qt-kill-ring-push! *isearch-active* isearch-handle-key! @@ -623,6 +625,15 @@ (when ts (terminal-stop! ts) (hash-remove! *terminal-state* buf))) + ;; Clean up QTerminalSession (PTY + all views) if applicable. + ;; Destroying the session detaches every view from its container, + ;; so later window teardown can't double-free them. + (let ((session (hash-get *terminal-widget-map* buf))) + (when session + (with-catch (lambda (e) #f) + (lambda () (qt-terminal-destroy! session))) + (hash-remove! *terminal-widget-map* buf) + (hash-remove! *terminal-views-map* buf))) ;; Clean up chat state if applicable (let ((cs (hash-get *chat-state* buf))) (when cs @@ -666,6 +677,12 @@ (when ss (shell-stop! ss) (hash-remove! *shell-state* buf))) (let ((ts (hash-get *terminal-state* buf))) (when ts (terminal-stop! ts) (hash-remove! *terminal-state* buf))) + (let ((session (hash-get *terminal-widget-map* buf))) + (when session + (with-catch (lambda (e) #f) + (lambda () (qt-terminal-destroy! session))) + (hash-remove! *terminal-widget-map* buf) + (hash-remove! *terminal-views-map* buf))) (let ((cs (hash-get *chat-state* buf))) (when cs (chat-stop! cs) (hash-remove! *chat-state* buf))) (set! *buffer-recent* --- a/src/jerboa-emacs/qt/sci-shim.ss +++ b/src/jerboa-emacs/qt/sci-shim.ss @@ -137,13 +137,14 @@ QT_CURSOR_START_OF_BLOCK QT_CURSOR_END_OF_BLOCK QT_CURSOR_NEXT_CHAR QT_CURSOR_NEXT_WORD QT_CURSOR_PREVIOUS_CHAR QT_CURSOR_PREVIOUS_WORD - ;; QTerminalWidget (libvterm-based terminal emulator) + ;; QTerminalSession / QTerminalView (libvterm-based terminal emulator) qt-terminal-create qt-terminal-destroy! qt-terminal-spawn! qt-terminal-connect-fd! qt-terminal-send-key-event! qt-terminal-send-input! qt-terminal-is-running? qt-terminal-interrupt! qt-terminal-set-font! qt-terminal-set-colors! - qt-terminal-focus! qt-terminal-widget) + qt-terminal-focus! qt-terminal-widget + qt-terminal-view-create qt-terminal-view-destroy! qt-terminal-view-focus!) (import :jerboa-emacs/core @@ -649,5 +650,20 @@ ((foreign-procedure "qt_terminal_focus" (void*) void) term)) (def (qt-terminal-widget term) - "Return the QWidget* pointer for the terminal (for adding to QStackedWidget)." + "Return a representative view (active-or-first) of the terminal SESSION as a + QWidget* pointer. Compatibility shim — prefer qt-terminal-view-* for views." ((foreign-procedure "qt_terminal_widget" (void*) void*) term)) + +(def (qt-terminal-view-create session parent) + "Create a QTerminalView of SESSION parented into PARENT (a QStackedWidget). + Registers the view with the session; returns the view's QWidget* pointer." + ((foreign-procedure "qt_terminal_view_create" (void* void*) void*) session parent)) + +(def (qt-terminal-view-destroy! view) + "Destroy a single terminal VIEW (detach from its stack, unregister, delete). + The owning session and its PTY/shell are left running." + ((foreign-procedure "qt_terminal_view_destroy" (void*) void) view)) + +(def (qt-terminal-view-focus! view) + "Give keyboard focus to a specific terminal VIEW." + ((foreign-procedure "qt_terminal_view_focus" (void*) void) view)) --- a/src/jerboa-emacs/qt/window.ss +++ b/src/jerboa-emacs/qt/window.ss @@ -42,7 +42,9 @@ ;; App pointer for process-events during splits qt-window-set-app-ptr! ;; Hook called before any window container is destroyed - qt-window-set-pre-container-destroy-fn!) + qt-window-set-pre-container-destroy-fn! + ;; Hook to override focus target after a split (e.g. terminal views) + qt-window-set-post-split-focus-fn!) (import :std/sugar :jerboa-scintilla/constants @@ -68,6 +70,15 @@ (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. +(def *post-split-focus-fn* #f) + +(def (qt-window-set-post-split-focus-fn! fn) + (set! *post-split-focus-fn* fn)) + (def (qt-window-pre-container-destroy! container) (when *pre-container-destroy-fn* (*pre-container-destroy-fn* container))) @@ -522,6 +533,12 @@ (lna (qt-line-number-area-create new-ed)) (new-win (make-qt-edit-window new-ed container buf lna #f #f))) (qt-scintilla-setup-editor! new-ed) + ;; Add the editor as stacked page 0 and register the window BEFORE attaching + ;; the buffer. qt-buffer-attach! runs the post-buffer-attach hook, which for a + ;; terminal buffer adds a QTerminalView as a later page — it needs the editor + ;; already present as page 0 and the window resolvable via *editor-window-map*. + (qt-stacked-widget-add-widget! container new-ed) + (hash-put! *editor-window-map* new-ed new-win) (qt-buffer-attach! new-ed buf) ;; Force font family+size on all styles (0-127). ;; QsciLexer's setLexer() overrides style fonts, so we must re-apply @@ -540,13 +557,11 @@ (when (and ln-face (face-fg ln-face)) (let-values (((r g b) (parse-hex-color (face-fg ln-face)))) (sci-send new-ed SCI_STYLESETFORE STYLE_LINENUMBER (rgb->sci r g b))))) - (qt-stacked-widget-add-widget! container new-ed) (if insert-index (qt-safe-splitter-insert-widget! "qt-make-new-window!" container-parent insert-index container) (qt-safe-splitter-add-widget! "qt-make-new-window!" container-parent container)) - (hash-put! *editor-window-map* new-ed new-win) new-win)) ;;;============================================================================ @@ -562,10 +577,12 @@ (lna (qt-line-number-area-create editor)) (win (make-qt-edit-window editor container buf lna #f #f))) (qt-scintilla-setup-editor! editor) - (qt-buffer-attach! editor buf) + ;; Register the window and add the editor as page 0 before attaching the + ;; buffer (keeps the post-buffer-attach hook's window/page lookups valid). (qt-stacked-widget-add-widget! container editor) - (qt-safe-splitter-add-widget! "qt-frame-init!" splitter container) (hash-put! *editor-window-map* editor win) + (qt-buffer-attach! editor buf) + (qt-safe-splitter-add-widget! "qt-frame-init!" splitter container) (let* ((root (make-split-leaf win)) (fr (make-qt-frame splitter root (list win) 0 main-win))) ;; Set initial visual indicator for active window @@ -591,13 +608,10 @@ (cur-leaf (split-tree-find-leaf (qt-frame-root fr) cur-win)) (parent (split-tree-find-parent (qt-frame-root fr) cur-win)) (cur-buf (qt-edit-window-buffer cur-win)) - ;; Terminal buffers use a QTerminalWidget that can only live in one pane. - ;; Use scratch buffer for the new pane instead of the terminal buffer. - (new-buf (if (eq? (buffer-lexer-lang cur-buf) 'terminal) - (or (find (lambda (b) (string=? (buffer-name b) buffer-scratch-name)) - (map qt-edit-window-buffer (qt-frame-windows fr))) - cur-buf) - cur-buf)) + ;; The new pane shows the SAME buffer as the current one — including + ;; terminal buffers, which now share one QTerminalSession across + ;; per-window QTerminalViews, so the split shows the same live terminal. + (new-buf cur-buf) ;; Save main window geometry — adding widgets to a QSplitter can cause ;; Qt to resize the QMainWindow via sizeHint propagation. (main-win (qt-frame-main-win fr)) @@ -713,8 +727,15 @@ ;; Restore main window size — prevent Qt from growing the window (when (and main-win saved-w saved-h)