Skip to content

Commit d07f509

Browse files
committed
Add full scrollback copy mode and copy-all command
Copy mode previously only allowed selecting text within a single viewport. Scrolling erased and redrawed the buffer, invalidating any active mark/region. Two new capabilities: - `ghostel-copy-all` (C-c M-w): Instantly copies the entire scrollback to the kill ring using the ghostty formatter API. No rendering needed. - `ghostel-copy-mode-load-all` (C-c C-a in copy mode): Loads the full scrollback into the buffer as styled text. After loading, standard Emacs selection works across the entire history (C-x h, C-s, mark/region spanning any distance). Scroll commands switch to native Emacs scrolling in this mode. Copy mode entry remains instant with no behavior change for quick selections.
1 parent 6075b64 commit d07f509

7 files changed

Lines changed: 480 additions & 100 deletions

File tree

README.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ test "$INSIDE_EMACS" = 'ghostel'; and source "$EMACS_GHOSTEL_PATH/etc/ghostel.fi
166166
| `C-c C-d` | Send EOF (C-d) |
167167
| `C-c C-\` | Send quit (C-\) |
168168
| `C-c C-t` | Enter copy mode |
169+
| `C-c M-w` | Copy entire scrollback to kill ring |
169170
| `C-y` | Yank from kill ring (bracketed paste) |
170171
| `M-y` | Yank-pop (cycle through kill ring) |
171172
| `C-c C-y` | Paste from kill ring |
@@ -183,21 +184,26 @@ Keys listed in `ghostel-keymap-exceptions` (default: `C-c`, `C-x`, `C-u`,
183184
Enter with `C-c C-t`. Standard Emacs navigation works.
184185
Normal letter keys exit copy mode and send the key to the terminal.
185186

186-
| Key | Action |
187-
|---------------|---------------------------------|
188-
| `C-SPC` | Set mark |
189-
| `M-w` / `C-w` | Copy selection and exit |
190-
| `C-n` / `C-p` | Move line (scrolls at edges) |
191-
| `M-v` / `C-v` | Scroll page up / down |
192-
| `M-<` / `M->` | Jump to top / bottom of buffer |
193-
| `C-c C-n` | Jump to next prompt |
194-
| `C-c C-p` | Jump to previous prompt |
195-
| `C-l` | Recenter viewport |
196-
| `C-c C-t` | Exit without copying |
197-
| `a``z` | Exit and send key to terminal |
187+
| Key | Action |
188+
|---------------|----------------------------------|
189+
| `C-SPC` | Set mark |
190+
| `M-w` / `C-w` | Copy selection and exit |
191+
| `C-n` / `C-p` | Move line (scrolls at edges) |
192+
| `M-v` / `C-v` | Scroll page up / down |
193+
| `M-<` / `M->` | Jump to top / bottom of buffer |
194+
| `C-c C-n` | Jump to next prompt |
195+
| `C-c C-p` | Jump to previous prompt |
196+
| `C-l` | Recenter viewport |
197+
| `C-c C-a` | Load full scrollback into buffer |
198+
| `C-c C-t` | Exit without copying |
199+
| `a``z` | Exit and send key to terminal |
198200

199201
Soft-wrapped newlines are automatically stripped from copied text.
200202

203+
After `C-c C-a`, the entire scrollback history is loaded into the buffer
204+
as styled text. Standard Emacs commands work across the full content:
205+
`C-x h` to select all, `C-s` to search, mark/region spanning any distance.
206+
201207
## Features
202208

203209
### Terminal Emulation
@@ -329,6 +335,7 @@ individual faces with `M-x customize-face`.
329335
| `M-x ghostel-clear` | Clear screen and scrollback |
330336
| `M-x ghostel-clear-scrollback` | Clear scrollback only |
331337
| `M-x ghostel-copy-mode` | Enter copy mode |
338+
| `M-x ghostel-copy-all` | Copy entire scrollback to kill ring |
332339
| `M-x ghostel-paste` | Paste from kill ring |
333340
| `M-x ghostel-send-next-key` | Send next key literally |
334341
| `M-x ghostel-next-prompt` | Jump to next shell prompt |

ghostel.el

Lines changed: 150 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,11 @@ Bump this only when the Elisp code requires a newer native module
340340
(declare-function ghostel--encode-key "ghostel-module")
341341
(declare-function ghostel--focus-event "ghostel-module")
342342
(declare-function ghostel--mode-enabled "ghostel-module")
343+
(declare-function ghostel--copy-all-text "ghostel-module")
343344
(declare-function ghostel--module-version "ghostel-module")
344345
(declare-function ghostel--mouse-event "ghostel-module")
345346
(declare-function ghostel--new "ghostel-module")
347+
(declare-function ghostel--redraw-full-scrollback "ghostel-module")
346348
(declare-function ghostel--redraw "ghostel-module" (term &optional full))
347349
(declare-function ghostel--scroll "ghostel-module")
348350
(declare-function ghostel--scroll-bottom "ghostel-module")
@@ -556,6 +558,9 @@ DIR is the module directory."
556558
(defvar-local ghostel--copy-mode-active nil
557559
"Non-nil when copy mode is active.")
558560

561+
(defvar-local ghostel--copy-mode-full-buffer nil
562+
"Non-nil when full scrollback has been loaded into the buffer in copy mode.")
563+
559564
(defvar-local ghostel--process nil
560565
"The shell process.")
561566

@@ -647,6 +652,7 @@ Used for prompt navigation and optional re-application after full redraws.")
647652
(define-key map (kbd "C-c C-\\") #'ghostel-send-C-backslash)
648653
(define-key map (kbd "C-c C-d") #'ghostel-send-C-d)
649654
(define-key map (kbd "C-c C-t") #'ghostel-copy-mode)
655+
(define-key map (kbd "C-c M-w") #'ghostel-copy-all)
650656
(define-key map (kbd "C-c C-y") #'ghostel-paste)
651657
(define-key map (kbd "C-c C-l") #'ghostel-clear-scrollback)
652658
(define-key map (kbd "C-c C-q") #'ghostel-send-next-key)
@@ -1004,89 +1010,107 @@ pasted using bracketed paste."
10041010
(defun ghostel--scroll-up (&optional _event)
10051011
"Scroll the terminal viewport up (into scrollback)."
10061012
(interactive "e")
1007-
(when ghostel--term
1008-
(ghostel--scroll ghostel--term -3)
1009-
(if ghostel--copy-mode-active
1010-
(let ((inhibit-read-only t))
1011-
(ghostel--redraw ghostel--term ghostel-full-redraw))
1012-
(setq ghostel--force-next-redraw t)
1013-
(ghostel--invalidate))))
1013+
(if ghostel--copy-mode-full-buffer
1014+
(scroll-down 3)
1015+
(when ghostel--term
1016+
(ghostel--scroll ghostel--term -3)
1017+
(if ghostel--copy-mode-active
1018+
(let ((inhibit-read-only t))
1019+
(ghostel--redraw ghostel--term ghostel-full-redraw))
1020+
(setq ghostel--force-next-redraw t)
1021+
(ghostel--invalidate)))))
10141022

10151023
(defun ghostel--scroll-down (&optional _event)
10161024
"Scroll the terminal viewport down."
10171025
(interactive "e")
1018-
(when ghostel--term
1019-
(ghostel--scroll ghostel--term 3)
1020-
(if ghostel--copy-mode-active
1021-
(let ((inhibit-read-only t))
1022-
(ghostel--redraw ghostel--term ghostel-full-redraw))
1023-
(setq ghostel--force-next-redraw t)
1024-
(ghostel--invalidate))))
1026+
(if ghostel--copy-mode-full-buffer
1027+
(scroll-up 3)
1028+
(when ghostel--term
1029+
(ghostel--scroll ghostel--term 3)
1030+
(if ghostel--copy-mode-active
1031+
(let ((inhibit-read-only t))
1032+
(ghostel--redraw ghostel--term ghostel-full-redraw))
1033+
(setq ghostel--force-next-redraw t)
1034+
(ghostel--invalidate)))))
10251035

10261036
(defun ghostel-copy-mode-scroll-up ()
10271037
"Scroll the terminal viewport up by a page in copy mode."
10281038
(interactive)
1029-
(when ghostel--term
1030-
(let ((height (count-lines (point-min) (point-max))))
1031-
(ghostel--scroll ghostel--term (- 2 height))
1032-
(let ((inhibit-read-only t))
1033-
(ghostel--redraw ghostel--term ghostel-full-redraw)))))
1039+
(if ghostel--copy-mode-full-buffer
1040+
(scroll-down-command)
1041+
(when ghostel--term
1042+
(let ((height (count-lines (point-min) (point-max))))
1043+
(ghostel--scroll ghostel--term (- 2 height))
1044+
(let ((inhibit-read-only t))
1045+
(ghostel--redraw ghostel--term ghostel-full-redraw))))))
10341046

10351047
(defun ghostel-copy-mode-scroll-down ()
10361048
"Scroll the terminal viewport down by a page in copy mode."
10371049
(interactive)
1038-
(when ghostel--term
1039-
(let ((height (count-lines (point-min) (point-max))))
1040-
(ghostel--scroll ghostel--term (- height 2))
1041-
(let ((inhibit-read-only t))
1042-
(ghostel--redraw ghostel--term ghostel-full-redraw)))))
1050+
(if ghostel--copy-mode-full-buffer
1051+
(scroll-up-command)
1052+
(when ghostel--term
1053+
(let ((height (count-lines (point-min) (point-max))))
1054+
(ghostel--scroll ghostel--term (- height 2))
1055+
(let ((inhibit-read-only t))
1056+
(ghostel--redraw ghostel--term ghostel-full-redraw))))))
10431057

10441058
(defun ghostel-copy-mode-previous-line ()
10451059
"Move to the previous line, scrolling the viewport if at the top."
10461060
(interactive)
10471061
(let ((col (current-column)))
1048-
(if (= (line-number-at-pos) 1)
1049-
(when ghostel--term
1050-
(ghostel--scroll ghostel--term -1)
1051-
(let ((inhibit-read-only t))
1052-
(ghostel--redraw ghostel--term ghostel-full-redraw))
1053-
(goto-char (point-min)))
1054-
(forward-line -1))
1062+
(if ghostel--copy-mode-full-buffer
1063+
(forward-line -1)
1064+
(if (= (line-number-at-pos) 1)
1065+
(when ghostel--term
1066+
(ghostel--scroll ghostel--term -1)
1067+
(let ((inhibit-read-only t))
1068+
(ghostel--redraw ghostel--term ghostel-full-redraw))
1069+
(goto-char (point-min)))
1070+
(forward-line -1)))
10551071
(move-to-column col)))
10561072

10571073
(defun ghostel-copy-mode-next-line ()
10581074
"Move to the next line, scrolling the viewport if at the bottom."
10591075
(interactive)
10601076
(let ((col (current-column)))
1061-
(if (>= (line-number-at-pos) (line-number-at-pos (point-max)))
1062-
(when ghostel--term
1063-
(ghostel--scroll ghostel--term 1)
1064-
(let ((inhibit-read-only t))
1065-
(ghostel--redraw ghostel--term ghostel-full-redraw))
1066-
(goto-char (point-max))
1067-
(beginning-of-line))
1068-
(forward-line 1))
1077+
(if ghostel--copy-mode-full-buffer
1078+
(forward-line 1)
1079+
(if (>= (line-number-at-pos) (line-number-at-pos (point-max)))
1080+
(when ghostel--term
1081+
(ghostel--scroll ghostel--term 1)
1082+
(let ((inhibit-read-only t))
1083+
(ghostel--redraw ghostel--term ghostel-full-redraw))
1084+
(goto-char (point-max))
1085+
(beginning-of-line))
1086+
(forward-line 1)))
10691087
(move-to-column col)))
10701088

10711089
(defun ghostel-copy-mode-beginning-of-buffer ()
10721090
"Scroll to the top of scrollback in copy mode."
10731091
(interactive)
1074-
(when ghostel--term
1075-
(ghostel--scroll-top ghostel--term)
1076-
(let ((inhibit-read-only t))
1077-
(ghostel--redraw ghostel--term ghostel-full-redraw))
1078-
(goto-char (point-min))))
1092+
(if ghostel--copy-mode-full-buffer
1093+
(goto-char (point-min))
1094+
(when ghostel--term
1095+
(ghostel--scroll-top ghostel--term)
1096+
(let ((inhibit-read-only t))
1097+
(ghostel--redraw ghostel--term ghostel-full-redraw))
1098+
(goto-char (point-min)))))
10791099

10801100
(defun ghostel-copy-mode-end-of-buffer ()
10811101
"Scroll to the bottom of scrollback in copy mode."
10821102
(interactive)
1083-
(when ghostel--term
1084-
(ghostel--scroll-bottom ghostel--term)
1085-
(let ((inhibit-read-only t))
1086-
(ghostel--redraw ghostel--term ghostel-full-redraw))
1087-
;; The native redraw already positions point at the terminal cursor,
1088-
;; so no explicit goto-char needed here.
1089-
))
1103+
(if ghostel--copy-mode-full-buffer
1104+
(progn
1105+
(goto-char (point-max))
1106+
(skip-chars-backward " \t\n"))
1107+
(when ghostel--term
1108+
(ghostel--scroll-bottom ghostel--term)
1109+
(let ((inhibit-read-only t))
1110+
(ghostel--redraw ghostel--term ghostel-full-redraw))
1111+
;; The native redraw already positions point at the terminal cursor,
1112+
;; so no explicit goto-char needed here.
1113+
)))
10901114

10911115
(defun ghostel-copy-mode-end-of-line ()
10921116
"Move to the last non-whitespace character on the line."
@@ -1100,30 +1124,32 @@ Scrolls the terminal viewport so the current line is vertically
11001124
centered, then redraws. When the scroll is clamped at a scrollback
11011125
boundary (nothing to scroll into), does nothing."
11021126
(interactive)
1103-
(when ghostel--term
1104-
(let* ((current-line (line-number-at-pos))
1105-
(win-height (window-body-height))
1106-
(center (/ win-height 2))
1107-
(col (current-column)))
1108-
(unless (= current-line center)
1109-
;; Hash the buffer to detect whether the scroll was clamped.
1110-
(let ((old-hash (buffer-hash)))
1111-
(ghostel--scroll ghostel--term (- current-line center))
1112-
(let ((inhibit-read-only t))
1113-
(ghostel--redraw ghostel--term ghostel-full-redraw))
1114-
;; If the buffer changed the viewport actually moved —
1115-
;; reposition point at center. Otherwise the scroll was
1116-
;; clamped; restore point since redraw moved it to the
1117-
;; terminal cursor.
1118-
(if (equal old-hash (buffer-hash))
1119-
(progn
1120-
(goto-char (point-min))
1121-
(forward-line (1- current-line))
1122-
(move-to-column col))
1123-
(goto-char (point-min))
1124-
(forward-line (1- (min center (line-number-at-pos (point-max)))))
1125-
(move-to-column col)
1126-
(recenter)))))))
1127+
(if ghostel--copy-mode-full-buffer
1128+
(recenter)
1129+
(when ghostel--term
1130+
(let* ((current-line (line-number-at-pos))
1131+
(win-height (window-body-height))
1132+
(center (/ win-height 2))
1133+
(col (current-column)))
1134+
(unless (= current-line center)
1135+
;; Hash the buffer to detect whether the scroll was clamped.
1136+
(let ((old-hash (buffer-hash)))
1137+
(ghostel--scroll ghostel--term (- current-line center))
1138+
(let ((inhibit-read-only t))
1139+
(ghostel--redraw ghostel--term ghostel-full-redraw))
1140+
;; If the buffer changed the viewport actually moved —
1141+
;; reposition point at center. Otherwise the scroll was
1142+
;; clamped; restore point since redraw moved it to the
1143+
;; terminal cursor.
1144+
(if (equal old-hash (buffer-hash))
1145+
(progn
1146+
(goto-char (point-min))
1147+
(forward-line (1- current-line))
1148+
(move-to-column col))
1149+
(goto-char (point-min))
1150+
(forward-line (1- (min center (line-number-at-pos (point-max)))))
1151+
(move-to-column col)
1152+
(recenter))))))))
11271153

11281154

11291155
;;; Mouse input
@@ -1214,6 +1240,7 @@ boundary (nothing to scroll into), does nothing."
12141240
(define-key map (kbd "M->") #'ghostel-copy-mode-end-of-buffer)
12151241
(define-key map (kbd "C-e") #'ghostel-copy-mode-end-of-line)
12161242
(define-key map (kbd "C-l") #'ghostel-copy-mode-recenter)
1243+
(define-key map (kbd "C-c C-a") #'ghostel-copy-mode-load-all)
12171244
map)
12181245
"Keymap for `ghostel-copy-mode'.
12191246
Standard Emacs navigation works.
@@ -1259,19 +1286,26 @@ Press \\`q' or \\[ghostel-copy-mode-exit] to exit without copying."
12591286
"Exit copy mode and return to terminal mode."
12601287
(interactive)
12611288
(when ghostel--copy-mode-active
1262-
(setq ghostel--copy-mode-active nil)
1263-
(setq cursor-type ghostel--saved-cursor-type)
1264-
(deactivate-mark)
1265-
(use-local-map ghostel--saved-local-map)
1266-
(when ghostel--saved-hl-line-mode
1267-
(hl-line-mode -1))
1268-
(setq buffer-read-only nil)
1269-
(setq mode-line-process nil)
1270-
(force-mode-line-update)
1271-
(when ghostel--term
1272-
(ghostel--scroll-bottom ghostel--term))
1273-
(ghostel--invalidate)
1274-
(message "Copy mode exited")))
1289+
(let ((was-full ghostel--copy-mode-full-buffer))
1290+
(setq ghostel--copy-mode-active nil)
1291+
(setq ghostel--copy-mode-full-buffer nil)
1292+
(setq cursor-type ghostel--saved-cursor-type)
1293+
(deactivate-mark)
1294+
(use-local-map ghostel--saved-local-map)
1295+
(when ghostel--saved-hl-line-mode
1296+
(hl-line-mode -1))
1297+
(setq buffer-read-only nil)
1298+
(setq mode-line-process nil)
1299+
(force-mode-line-update)
1300+
(when ghostel--term
1301+
(ghostel--scroll-bottom ghostel--term)
1302+
(when was-full
1303+
;; Erase stale full-scrollback content so normal redraw rebuilds
1304+
(let ((inhibit-read-only t))
1305+
(erase-buffer)
1306+
(ghostel--redraw ghostel--term t))))
1307+
(ghostel--invalidate)
1308+
(message "Copy mode exited"))))
12751309

12761310
(defun ghostel-copy-mode-exit-and-send ()
12771311
"Exit copy mode and send the key that triggered exit to the terminal."
@@ -1313,6 +1347,34 @@ stripped so the copied text matches the original terminal content."
13131347
(message "Copied to kill ring")))
13141348
(ghostel-copy-mode-exit))
13151349

1350+
(defun ghostel-copy-mode-load-all ()
1351+
"Load the entire scrollback into the buffer for cross-viewport selection.
1352+
After loading, standard Emacs navigation and selection work across
1353+
the full scrollback history."
1354+
(interactive)
1355+
(when (and ghostel--copy-mode-active ghostel--term
1356+
(not ghostel--copy-mode-full-buffer))
1357+
(message "Loading scrollback...")
1358+
(let* ((saved-line (1- (line-number-at-pos))) ; 0-based line within viewport
1359+
(saved-col (current-column))
1360+
(inhibit-read-only t)
1361+
(viewport-line (ghostel--redraw-full-scrollback ghostel--term)))
1362+
(goto-char (point-min))
1363+
(forward-line (+ (1- viewport-line) saved-line))
1364+
(move-to-column saved-col)
1365+
(recenter saved-line))
1366+
(setq ghostel--copy-mode-full-buffer t)
1367+
(message "Scrollback loaded")))
1368+
1369+
(defun ghostel-copy-all ()
1370+
"Copy the entire scrollback buffer to the kill ring."
1371+
(interactive)
1372+
(when ghostel--term
1373+
(let ((text (ghostel--copy-all-text ghostel--term)))
1374+
(when (and text (> (length text) 0))
1375+
(kill-new text)
1376+
(message "Copied %d characters to kill ring" (length text))))))
1377+
13161378

13171379
;;; Hyperlinks (OSC 8)
13181380

0 commit comments

Comments
 (0)