Implement native vterm copy mode (visible screen)
ober
ee40bbb2404bf8752f853f8a2089df0c9c0ac3de
--- a/lib/jerboa-emacs/qt/app.sls +++ b/lib/jerboa-emacs/qt/app.sls @@ -1947,11 +1947,15 @@ (lambda (fr) (let* ([win (qt-current-window fr)] [buf (qt-edit-window-buffer win)] + [container (qt-edit-window-container win)] [view (and buf (hash-get *terminal-widget-map* buf) + (> (qt-stacked-widget-current-index + container) + 0) (terminal-view-for-container buf - (qt-edit-window-container win)))]) + container))]) (or view (qt-current-editor fr))))) (qt-window-set-pre-container-destroy-fn! (lambda (container) --- a/lib/jerboa-emacs/qt/commands-aliases.sls +++ b/lib/jerboa-emacs/qt/commands-aliases.sls @@ -1120,6 +1120,9 @@ (register-command! 'multi-vterm cmd-multi-vterm) (register-command! 'vterm-copy-mode cmd-vterm-copy-mode) (register-command! 'vterm-copy-done cmd-vterm-copy-done) + (register-command! + 'vterm-copy-mode-done + cmd-vterm-copy-done) (register-command! 'lsp cmd-toggle-lsp) (register-command! 'lsp-start cmd-toggle-lsp) (register-command! --- a/lib/jerboa-emacs/qt/commands-config.sls +++ b/lib/jerboa-emacs/qt/commands-config.sls @@ -21,8 +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* cmd-vterm-copy-mode - cmd-vterm-copy-done get-terminal-buffers + cmd-multi-vterm *terminal-copy-mode* 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) (import @@ -1174,35 +1174,71 @@ (cmd-term app)) (define *terminal-copy-mode*--cell (vector (make-hash-table))) + (def (vterm-native-session buf) + "Return the native QTerminalSession backing BUF, or #f." + (and buf (hash-get *terminal-widget-map* buf))) (def (cmd-vterm-copy-mode app) - "Toggle terminal copy mode — makes terminal read-only for text selection." - (let* ([buf (current-qt-buffer app)] + "Enter terminal copy mode: freeze the visible terminal screen into the editor\n so you can move the cursor and select/copy text. Exit with vterm-copy-mode-done.\n For a native QTerminalSession vterm this swaps the live view for a read-only\n editor holding the screen text; the editor focus disengages the PTY fast-path\n so ordinary motion (C-p/C-n/arrows) and mark/region (C-SPC … M-w) just work." + (let* ([fr (app-state-frame app)] + [win (qt-current-window fr)] + [buf (current-qt-buffer app)] [ed (current-qt-editor app)] - [echo (app-state-echo app)]) - (if (terminal-buffer? buf) - (let ([in-copy (hash-get *terminal-copy-mode* buf)]) - (if in-copy - (begin - (hash-put! *terminal-copy-mode* buf #f) - (qt-plain-text-edit-set-read-only! ed #f) - (echo-message! echo "Terminal copy mode OFF")) - (begin - (hash-put! *terminal-copy-mode* buf #t) - (qt-plain-text-edit-set-read-only! ed #t) - (echo-message! - echo - "Terminal copy mode ON — select text, C-w/M-w to copy")))) - (echo-message! echo "Not in a terminal buffer")))) + [echo (app-state-echo app)] + [session (vterm-native-session buf)]) + (cond + [session + (if (hash-get *terminal-copy-mode* buf) + (echo-message! + echo + "Already in copy mode (vterm-copy-mode-done to exit)") + (let ([container (qt-edit-window-container win)] + [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-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")))] + [(terminal-buffer? buf) + (hash-put! *terminal-copy-mode* buf #t) + (qt-plain-text-edit-set-read-only! ed #t) + (echo-message! + echo + "Terminal copy mode ON — select text, C-w/M-w to copy")] + [else (echo-message! echo "Not in a terminal buffer")]))) (def (cmd-vterm-copy-done app) - "Exit terminal copy mode and resume terminal." - (let* ([buf (current-qt-buffer app)] + "Exit terminal copy mode: copy any active selection to the kill ring, then\n return to the live terminal." + (let* ([fr (app-state-frame app)] + [win (qt-current-window fr)] + [buf (current-qt-buffer app)] [ed (current-qt-editor app)] - [echo (app-state-echo app)]) - (when (and (terminal-buffer? buf) - (hash-get *terminal-copy-mode* buf)) - (hash-put! *terminal-copy-mode* buf #f) - (qt-plain-text-edit-set-read-only! ed #f) - (echo-message! echo "Terminal copy mode OFF")))) + [echo (app-state-echo app)] + [session (vterm-native-session buf)]) + (cond + [(and session (hash-get *terminal-copy-mode* buf)) + (when (qt-plain-text-edit-has-selection? ed) + (let ([sel (qt-plain-text-edit-selected-text ed)]) + (when (and sel (> (string-length sel) 0)) + (qt-kill-ring-push! app sel) + (qt-plain-text-edit-copy! ed)))) + (qt-plain-text-edit-set-read-only! ed #f) + (let* ([container (qt-edit-window-container win)] + [view (terminal-view-for-container buf container)]) + (when view + (qt-stacked-widget-set-current-widget! container view) + (qt-terminal-view-focus! view))) + (hash-put! *terminal-copy-mode* buf #f) + (echo-message! echo "Copy mode done")] + [(and (terminal-buffer? buf) + (hash-get *terminal-copy-mode* buf)) + (hash-put! *terminal-copy-mode* buf #f) + (qt-plain-text-edit-set-read-only! ed #f) + (echo-message! echo "Terminal copy mode OFF")] + [else (echo-message! echo "Not in copy mode")]))) (def (get-terminal-buffers) "Return list of terminal buffers from buffer-list." (filter terminal-buffer? *buffer-list*)) --- a/lib/jerboa-emacs/qt/sci-shim.sls +++ b/lib/jerboa-emacs/qt/sci-shim.sls @@ -140,10 +140,11 @@ 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-view-create - qt-terminal-view-destroy! qt-terminal-view-focus!) + 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!) (import (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex @@ -655,6 +656,12 @@ (= 1 ((foreign-procedure "qt_terminal_is_running" (void*) int) term))) + (def (qt-terminal-get-screen-text term) + "Return the visible terminal screen as plain text (for copy mode)." + ((foreign-procedure "qt_terminal_get_screen_text" + (void*) + string) + term)) (def (qt-terminal-interrupt! term) "Send SIGINT to the terminal's child process." ((foreign-procedure "qt_terminal_interrupt" (void*) void) --- a/src/jerboa-emacs/qt/app.ss +++ b/src/jerboa-emacs/qt/app.ss @@ -1506,10 +1506,14 @@ (lambda (fr) (let* ((win (qt-current-window fr)) (buf (qt-edit-window-buffer win)) + (container (qt-edit-window-container win)) + ;; Only target the live terminal view when it is actually the + ;; shown widget (stack page > 0). In vterm copy mode the stack + ;; shows the editor (page 0), so keys must go to the editor. (view (and buf (hash-get *terminal-widget-map* buf) - (terminal-view-for-container - buf (qt-edit-window-container win))))) + (> (qt-stacked-widget-current-index container) 0) + (terminal-view-for-container buf container)))) (or view (qt-current-editor fr))))) ;; Install pre-container-destroy hook: when any window container --- a/src/jerboa-emacs/qt/commands-aliases.ss +++ b/src/jerboa-emacs/qt/commands-aliases.ss @@ -822,6 +822,7 @@ (register-command! 'multi-vterm cmd-multi-vterm) (register-command! 'vterm-copy-mode cmd-vterm-copy-mode) (register-command! 'vterm-copy-done cmd-vterm-copy-done) + (register-command! 'vterm-copy-mode-done cmd-vterm-copy-done) ;; emacs-libvterm name ;; Image mode commands — moved to commands-aliases2.ss ;; LSP commands (register-command! 'lsp cmd-toggle-lsp) ; alias: M-x lsp --- a/src/jerboa-emacs/qt/commands-config.ss +++ b/src/jerboa-emacs/qt/commands-config.ss @@ -998,35 +998,77 @@ 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 (vterm-native-session buf) + "Return the native QTerminalSession backing BUF, or #f." + (and buf (hash-get *terminal-widget-map* buf))) + (def (cmd-vterm-copy-mode app) - "Toggle terminal copy mode — makes terminal read-only for text selection." - (let* ((buf (current-qt-buffer app)) + "Enter terminal copy mode: freeze the visible terminal screen into the editor + so you can move the cursor and select/copy text. Exit with vterm-copy-mode-done. + For a native QTerminalSession vterm this swaps the live view for a read-only + editor holding the screen text; the editor focus disengages the PTY fast-path + so ordinary motion (C-p/C-n/arrows) and mark/region (C-SPC … M-w) just work." + (let* ((fr (app-state-frame app)) + (win (qt-current-window fr)) + (buf (current-qt-buffer app)) (ed (current-qt-editor app)) - (echo (app-state-echo app))) - (if (terminal-buffer? buf) - (let ((in-copy (hash-get *terminal-copy-mode* buf))) - (if in-copy - ;; Exit copy mode - (begin - (hash-put! *terminal-copy-mode* buf #f) - (qt-plain-text-edit-set-read-only! ed #f) - (echo-message! echo "Terminal copy mode OFF")) - ;; Enter copy mode - (begin - (hash-put! *terminal-copy-mode* buf #t) - (qt-plain-text-edit-set-read-only! ed #t) - (echo-message! echo "Terminal copy mode ON — select text, C-w/M-w to copy")))) - (echo-message! echo "Not in a terminal buffer")))) + (echo (app-state-echo app)) + (session (vterm-native-session buf))) + (cond + (session + (if (hash-get *terminal-copy-mode* buf) + (echo-message! echo "Already in copy mode (vterm-copy-mode-done to exit)") + (let ((container (qt-edit-window-container win)) + (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-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")))) + ;; Legacy line-terminal: read-only toggle on the editor (old behavior). + ((terminal-buffer? buf) + (hash-put! *terminal-copy-mode* buf #t) + (qt-plain-text-edit-set-read-only! ed #t) + (echo-message! echo "Terminal copy mode ON — select text, C-w/M-w to copy")) + (else + (echo-message! echo "Not in a terminal buffer"))))) (def (cmd-vterm-copy-done app) - "Exit terminal copy mode and resume terminal." - (let* ((buf (current-qt-buffer app)) + "Exit terminal copy mode: copy any active selection to the kill ring, then + return to the live terminal." + (let* ((fr (app-state-frame app)) + (win (qt-current-window fr)) + (buf (current-qt-buffer app)) (ed (current-qt-editor app)) - (echo (app-state-echo app))) - (when (and (terminal-buffer? buf) (hash-get *terminal-copy-mode* buf)) - (hash-put! *terminal-copy-mode* buf #f) - (qt-plain-text-edit-set-read-only! ed #f) - (echo-message! echo "Terminal copy mode OFF")))) + (echo (app-state-echo app)) + (session (vterm-native-session buf))) + (cond + ((and session (hash-get *terminal-copy-mode* buf)) + ;; Grab the active selection (if any) before leaving. + (when (qt-plain-text-edit-has-selection? ed) + (let ((sel (qt-plain-text-edit-selected-text ed))) + (when (and sel (> (string-length sel) 0)) + (qt-kill-ring-push! app sel) + (qt-plain-text-edit-copy! ed)))) ;; also to the system clipboard + (qt-plain-text-edit-set-read-only! ed #f) + ;; Restore the live terminal view for this window. + (let* ((container (qt-edit-window-container win)) + (view (terminal-view-for-container buf container))) + (when view + (qt-stacked-widget-set-current-widget! container view) + (qt-terminal-view-focus! view))) + (hash-put! *terminal-copy-mode* buf #f) + (echo-message! echo "Copy mode done")) + ;; Legacy line-terminal copy mode + ((and (terminal-buffer? buf) (hash-get *terminal-copy-mode* buf)) + (hash-put! *terminal-copy-mode* buf #f) + (qt-plain-text-edit-set-read-only! ed #f) + (echo-message! echo "Terminal copy mode OFF")) + (else + (echo-message! echo "Not in copy mode"))))) (def (get-terminal-buffers) "Return list of terminal buffers from buffer-list." --- a/src/jerboa-emacs/qt/sci-shim.ss +++ b/src/jerboa-emacs/qt/sci-shim.ss @@ -141,7 +141,7 @@ 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-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!) @@ -645,6 +645,10 @@ "Check if the terminal's child process is still running." (= 1 ((foreign-procedure "qt_terminal_is_running" (void*) int) term))) +(def (qt-terminal-get-screen-text term) + "Return the visible terminal screen as plain text (for copy mode)." + ((foreign-procedure "qt_terminal_get_screen_text" (void*) string) term)) + (def (qt-terminal-interrupt! term) "Send SIGINT to the terminal's child process." ((foreign-procedure "qt_terminal_interrupt" (void*) void) term)) --- a/support/vendor-overrides/qt_shim.cpp +++ b/support/vendor-overrides/qt_shim.cpp @@ -8266,6 +8266,42 @@ extern "C" int qt_terminal_is_running(qt_terminal_t term) { ); } +// Dump the visible terminal screen (rows x cols) as plain UTF-8 text, one row +// per line, trailing spaces and trailing blank lines stripped. Used by the +// Scheme `vterm-copy-mode` command to freeze the screen into an editor buffer +// so the user can move the cursor and select/copy text. (Scrollback is not +// retained — see cb_sb_pushline — so only the current screen is returned.) +extern "C" const char* qt_terminal_get_screen_text(qt_terminal_t term) { + QT_NULL_CHECK_RET(term, ""); + QTerminalSession* s = static_cast<QTerminalSession*>(term); + QT_RETURN_STRING(([&]() -> std::string { + VTermScreen* screen = s->m_screen; + if (!screen) return std::string(); + int rows = s->m_rows, cols = s->m_cols; + QString out; + for (int row = 0; row < rows; ++row) { + QString line; + for (int col = 0; col < cols; ++col) { + VTermPos pos = { row, col }; + VTermScreenCell cell; + vterm_screen_get_cell(screen, pos, &cell); + if (cell.chars[0] != 0) { + line += QString::fromUcs4(&cell.chars[0], 1); + if (cell.width > 1) col += (cell.width - 1); + } else { + line += QLatin1Char(' '); + } + } + while (line.endsWith(QLatin1Char(' '))) line.chop(1); + out += line; + out += QLatin1Char('\n'); + } + while (out.endsWith(QLatin1String("\n\n"))) out.chop(1); + if (out.endsWith(QLatin1Char('\n'))) out.chop(1); + return std::string(out.toUtf8().constData()); + })()); +} + // Compatibility shim: return a representative view (active-or-first) as a // QWidget*. The session itself is NOT a widget. Prefer the per-view FFI. extern "C" qt_widget_t qt_terminal_widget(qt_terminal_t term) {