Skip to content

Commit 31bdc9c

Browse files
committed
Snap point to buffer end on scroll-on-input
When point is in scrollback (e.g. after pixel-scroll-precision-mode, mouse wheel, M-v, or any scrolling command that moves point) and the user types or pastes, ghostel--scroll-bottom resets the terminal viewport but leaves Emacs point above viewport-start. The delayed redraw then preserves point via saved-marker and skips the set-window-start anchor, so the window stays showing scrollback. Factor the terminal-viewport reset + point snap + redraw bump into ghostel--snap-to-input, and call it from self-insert, send-event, and the shared paste/yank/drop path (ghostel--paste-text). The terminal-engine responsibility (scroll-bottom) stays in the native module; the Emacs-buffer responsibility (goto-char) stays in Elisp, with one composition point. Fixes #113
1 parent a7139d5 commit 31bdc9c

2 files changed

Lines changed: 71 additions & 21 deletions

File tree

ghostel.el

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,12 +1057,24 @@ Returns the sequence string, or nil for unknown keys."
10571057
(format "\e[%d;%d~" param (1+ mod-num))
10581058
(format "\e[%d~" param)))
10591059

1060+
(defun ghostel--snap-to-input ()
1061+
"Return the window to the live viewport on user input.
1062+
Resets the terminal engine's viewport out of scrollback and moves
1063+
Emacs point to `point-max' so the delayed redraw anchors
1064+
`window-start' to the viewport and clears any pixel vscroll left
1065+
by `pixel-scroll-precision-mode' or similar scrollers. No-op
1066+
when `ghostel-scroll-on-input' is nil. Call from any path where
1067+
the user's action implies \"show me the prompt\" — typed input,
1068+
paste, yank, drop."
1069+
(when (and ghostel-scroll-on-input ghostel--term)
1070+
(ghostel--scroll-bottom ghostel--term)
1071+
(goto-char (point-max))
1072+
(setq ghostel--force-next-redraw t)))
1073+
10601074
(defun ghostel--self-insert ()
10611075
"Send the last typed character to the terminal."
10621076
(interactive)
1063-
(when (and ghostel-scroll-on-input ghostel--term)
1064-
(ghostel--scroll-bottom ghostel--term)
1065-
(setq ghostel--force-next-redraw t))
1077+
(ghostel--snap-to-input)
10661078
(let* ((keys (this-command-keys))
10671079
(char (aref keys (1- (length keys))))
10681080
(str (if (and (characterp char) (< char 128))
@@ -1076,9 +1088,7 @@ Extracts the base key name and modifiers from `last-command-event'
10761088
and routes through the ghostty key encoder, which respects terminal
10771089
modes (application cursor keys, Kitty keyboard protocol, etc.)."
10781090
(interactive)
1079-
(when (and ghostel-scroll-on-input ghostel--term)
1080-
(ghostel--scroll-bottom ghostel--term)
1081-
(setq ghostel--force-next-redraw t))
1091+
(ghostel--snap-to-input)
10821092
(let* ((event last-command-event)
10831093
(base (event-basic-type event))
10841094
(mods (event-modifiers event))
@@ -1155,6 +1165,7 @@ Clears `quit-flag' which Emacs sets when \\`C-g' is pressed with
11551165
(defun ghostel--paste-text (text)
11561166
"Send TEXT to the terminal, using bracketed paste if the terminal wants it."
11571167
(when (and text ghostel--process (process-live-p ghostel--process))
1168+
(ghostel--snap-to-input)
11581169
(process-send-string ghostel--process
11591170
(if (ghostel--bracketed-paste-p)
11601171
(concat "\e[200~" text "\e[201~")

test/ghostel-test.el

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3703,12 +3703,16 @@ buffer and hand nil to the native module."
37033703
(lambda (_term) (setq scroll-bottom-called t)))
37043704
((symbol-function 'ghostel--send-key)
37053705
(lambda (str) (setq sent-key str))))
3706-
(let ((last-command-event ?a))
3707-
(cl-letf (((symbol-function 'this-command-keys) (lambda () "a")))
3708-
(ghostel--self-insert)))
3709-
(should scroll-bottom-called)
3710-
(should ghostel--force-next-redraw)
3711-
(should (equal "a" sent-key)))))
3706+
(with-temp-buffer
3707+
(insert "scrollback\nscrollback\nscrollback\n")
3708+
(goto-char (point-min))
3709+
(let ((last-command-event ?a))
3710+
(cl-letf (((symbol-function 'this-command-keys) (lambda () "a")))
3711+
(ghostel--self-insert)))
3712+
(should scroll-bottom-called)
3713+
(should ghostel--force-next-redraw)
3714+
(should (equal "a" sent-key))
3715+
(should (= (point) (point-max)))))))
37123716

37133717
(ert-deftest ghostel-test-scroll-on-input-send-event ()
37143718
"Send-event scrolls to bottom when `ghostel-scroll-on-input' is non-nil."
@@ -3720,10 +3724,14 @@ buffer and hand nil to the native module."
37203724
(lambda (_term) (setq scroll-bottom-called t)))
37213725
((symbol-function 'ghostel--send-encoded)
37223726
(lambda (_key _mods &optional _utf8) nil)))
3723-
(let ((last-command-event (aref (kbd "<return>") 0)))
3724-
(ghostel--send-event))
3725-
(should scroll-bottom-called)
3726-
(should ghostel--force-next-redraw))))
3727+
(with-temp-buffer
3728+
(insert "scrollback\nscrollback\nscrollback\n")
3729+
(goto-char (point-min))
3730+
(let ((last-command-event (aref (kbd "<return>") 0)))
3731+
(ghostel--send-event))
3732+
(should scroll-bottom-called)
3733+
(should ghostel--force-next-redraw)
3734+
(should (= (point) (point-max)))))))
37273735

37283736
(ert-deftest ghostel-test-scroll-on-input-disabled ()
37293737
"Self-insert does not scroll when `ghostel-scroll-on-input' is nil."
@@ -3735,11 +3743,41 @@ buffer and hand nil to the native module."
37353743
(lambda (_term) (setq scroll-bottom-called t)))
37363744
((symbol-function 'ghostel--send-key)
37373745
(lambda (_str) nil)))
3738-
(cl-letf (((symbol-function 'this-command-keys) (lambda () "a")))
3739-
(let ((last-command-event ?a))
3740-
(ghostel--self-insert)))
3741-
(should-not scroll-bottom-called)
3742-
(should-not ghostel--force-next-redraw))))
3746+
(with-temp-buffer
3747+
(insert "scrollback\nscrollback\nscrollback\n")
3748+
(goto-char (point-min))
3749+
(let ((start (point)))
3750+
(cl-letf (((symbol-function 'this-command-keys) (lambda () "a")))
3751+
(let ((last-command-event ?a))
3752+
(ghostel--self-insert)))
3753+
(should-not scroll-bottom-called)
3754+
(should-not ghostel--force-next-redraw)
3755+
(should (= (point) start)))))))
3756+
3757+
(ert-deftest ghostel-test-scroll-on-input-paste ()
3758+
"Paste via `ghostel--paste-text' snaps point to the prompt."
3759+
(let ((ghostel--term 'fake)
3760+
(ghostel--process 'fake-proc)
3761+
(ghostel--force-next-redraw nil)
3762+
(ghostel-scroll-on-input t)
3763+
(scroll-bottom-called nil)
3764+
(sent-text nil))
3765+
(cl-letf (((symbol-function 'ghostel--scroll-bottom)
3766+
(lambda (_term) (setq scroll-bottom-called t)))
3767+
((symbol-function 'ghostel--bracketed-paste-p)
3768+
(lambda () nil))
3769+
((symbol-function 'process-live-p)
3770+
(lambda (_p) t))
3771+
((symbol-function 'process-send-string)
3772+
(lambda (_p s) (setq sent-text s))))
3773+
(with-temp-buffer
3774+
(insert "scrollback\nscrollback\nscrollback\n")
3775+
(goto-char (point-min))
3776+
(ghostel--paste-text "hello")
3777+
(should scroll-bottom-called)
3778+
(should ghostel--force-next-redraw)
3779+
(should (equal "hello" sent-text))
3780+
(should (= (point) (point-max)))))))
37433781

37443782
(ert-deftest ghostel-test-scroll-intercept-forwards-mouse-tracking ()
37453783
"Scroll intercept forwards events when mouse tracking is active."
@@ -4349,6 +4387,7 @@ while :; do sleep 0.1; done'\n")
43494387
ghostel-test-scroll-on-input-self-insert
43504388
ghostel-test-scroll-on-input-send-event
43514389
ghostel-test-scroll-on-input-disabled
4390+
ghostel-test-scroll-on-input-paste
43524391
ghostel-test-scroll-intercept-forwards-mouse-tracking
43534392
ghostel-test-scroll-intercept-fallthrough
43544393
ghostel-test-control-key-bindings

0 commit comments

Comments
 (0)