Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion lisp/ghostel-line-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ in line mode (the interactive entry validates these)."
(setq ghostel--line-mode-on-alt-screen (ghostel--line-mode-alt-screen-p))
(setq ghostel--line-mode-paused nil)
(setq ghostel--input-mode 'line)
(ghostel--line-mode-enable-around-redraw)
(use-local-map ghostel-line-mode-map)
(setq ghostel--mode-line-tag (ghostel--mode-line-tag-make 'line ":Line"))
(ghostel--mode-line-refresh)
Expand Down Expand Up @@ -559,6 +560,7 @@ which discards any type-ahead and runs inside `ghostel--redraw-now'."
;; below so none of them are recorded.
(setq buffer-undo-list t)
(unless pause
(ghostel--line-mode-disable-around-redraw)
(let ((input (ghostel--line-mode-input-text)))
;; Erase the adopted prefix from the shell's readline before
;; handing back our (possibly edited) version, so the shell ends
Expand Down Expand Up @@ -628,6 +630,7 @@ already running. Stashes an empty snapshot so the alt-screen-off
transition picks it up and enters line mode for real. Preserves
any existing paused snapshot (the user re-arming should not clobber
input captured by an earlier auto-pause)."
(ghostel--line-mode-enable-around-redraw)
(unless ghostel--line-mode-paused
(setq ghostel--line-mode-paused
(list :input "" :point-offset nil :mark-offset nil)))
Expand Down Expand Up @@ -663,6 +666,37 @@ screen. Also drives the deferred startup entry when
(ghostel--line-mode-try-resume))
(ghostel--line-mode-maybe-enter-initial))

(defun ghostel--line-mode-enable-around-redraw ()
"Add line mode's buffer-local redraw wrapper."
(add-hook 'ghostel--around-redraw-functions
#'ghostel--line-mode-around-redraw 90 t))

(defun ghostel--line-mode-defer-initial-entry ()
"Arm line mode to enter when the first prompt renders."
(setq ghostel--pending-initial-line-mode t)
(ghostel--line-mode-enable-around-redraw))

(defun ghostel--line-mode-disable-around-redraw ()
"Remove line mode's buffer-local redraw wrapper."
(remove-hook 'ghostel--around-redraw-functions
#'ghostel--line-mode-around-redraw t))

(defun ghostel--line-mode-around-redraw (next)
"Run NEXT while preserving line-mode state across the redraw."
(ghostel--line-mode-pre-redraw)
(let* ((snapshot (ghostel--line-mode-snapshot))
(redrew (funcall next))
(restored (and snapshot
(ghostel--line-mode-restore snapshot))))
(when (and snapshot (not restored))
(let ((input (plist-get snapshot :input)))
(when (and input (> (length input) 0))
(ghostel--write-pty ghostel--term input))
(message "ghostel: line-mode prompt lost; input forwarded raw")))
(when redrew
(ghostel--line-mode-post-redraw)
t)))

(defun ghostel--line-mode-startup-prompt-ready-p ()
"Non-nil when the cursor row shows a real prompt (OSC 133 prop or regex).
Gates the deferred startup entry so line mode skips a blank pre-prompt screen.
Expand All @@ -687,7 +721,8 @@ wins: a non-semi-char buffer drops the flag without entering."
(when ghostel--pending-initial-line-mode
(cond
((not (eq ghostel--input-mode 'semi-char))
(setq ghostel--pending-initial-line-mode nil))
(setq ghostel--pending-initial-line-mode nil)
(ghostel--line-mode-disable-around-redraw))
((ghostel--line-mode-startup-prompt-ready-p)
(setq ghostel--pending-initial-line-mode nil)
(ghostel-line-mode)))))
Expand Down
140 changes: 83 additions & 57 deletions lisp/ghostel.el
Original file line number Diff line number Diff line change
Expand Up @@ -4499,6 +4499,40 @@ run the shell on the remote host."

;;; Rendering

(defvar ghostel--around-redraw-functions
'(ghostel--redraw-maybe-defer
ghostel--redraw-detect-password-prompt
ghostel--redraw-detect-links
ghostel--redraw-anchor-windows
ghostel--redraw-apply-cursor-style)
"Functions composing a Ghostel redraw.
Each function is called with one argument, NEXT. Calling NEXT runs the
next function in the chain; not calling NEXT suppresses the rest of the
redraw.

This is a hook-like variable. Buffer-local values may include the symbol
t to splice in the default value, matching normal local hook semantics.

Around-redraw functions should call NEXT synchronously and return its non-nil
value when NEXT reaches an actual redraw.")

(defun ghostel--run-around-redraw-list (functions redraw)
"Run REDRAW through around-redraw FUNCTIONS."
(if-let* ((function (car functions)))
(if (eq function t)
(ghostel--run-around-redraw-list
(default-value 'ghostel--around-redraw-functions)
(lambda ()
(ghostel--run-around-redraw-list (cdr functions) redraw)))
(funcall function
(lambda ()
(ghostel--run-around-redraw-list (cdr functions) redraw))))
(funcall redraw)))

(defun ghostel--run-around-redraw-functions (redraw)
"Run REDRAW through `ghostel--around-redraw-functions'."
(ghostel--run-around-redraw-list ghostel--around-redraw-functions redraw))

(defvar-local ghostel--last-output-time nil
"Time of the last process output, for adaptive frame rate.")

Expand Down Expand Up @@ -4672,16 +4706,46 @@ user's point, since its input region is user-owned."
orig
(or ghostel--cursor-char-pos target))))))))

(defun ghostel--maybe-defer-redraw (buffer)
"Defer BUFFER's redraw if a `ghostel-inhibit-redraw-functions' hook asks.
Return non-nil when deferred, after rescheduling `ghostel--redraw-now'
for BUFFER; return nil to let the redraw proceed."
(when (with-demoted-errors "ghostel-inhibit-redraw-functions error: %S"
(run-hook-with-args-until-success
'ghostel-inhibit-redraw-functions buffer))
(setq ghostel--redraw-timer
(run-with-timer ghostel-timer-delay nil
#'ghostel--redraw-now buffer))
(defun ghostel--redraw-maybe-defer (next)
"Call NEXT unless `ghostel-inhibit-redraw-functions' asks to defer."
(if (with-demoted-errors "ghostel-inhibit-redraw-functions error: %S"
(run-hook-with-args-until-success
'ghostel-inhibit-redraw-functions (current-buffer)))
(progn
(setq ghostel--redraw-timer
(run-with-timer ghostel-timer-delay nil
#'ghostel--redraw-now (current-buffer)))
nil)
(funcall next)))

(defun ghostel--redraw-detect-password-prompt (next)
"Run NEXT, then detect password prompts if a redraw happened."
(when (funcall next)
(ghostel--detect-password-prompt)
t))

(defun ghostel--redraw-detect-links (next)
"Run NEXT, then schedule redraw-triggered plain-text link detection."
(when (funcall next)
(ghostel--schedule-link-detection (ghostel--viewport-start)
(point-max))
t))

(defun ghostel--redraw-anchor-windows (next)
"Run NEXT, then re-anchor windows that followed the live viewport."
(let ((anchored (cl-remove-if-not #'ghostel--window-anchored-p
(get-buffer-window-list (current-buffer)
nil t))))
(when (funcall next)
(dolist (win anchored)
(when (window-live-p win)
(ghostel--anchor-window win)))
t)))

(defun ghostel--redraw-apply-cursor-style (next)
"Run NEXT, then apply the terminal-published cursor style."
(when (funcall next)
(ghostel--apply-cursor-style)
t))

(defun ghostel--redraw-now (buffer &optional force)
Expand All @@ -4700,63 +4764,25 @@ opportunistic output redraws that may safely wait for the frame to end."
(cancel-timer ghostel--redraw-timer)
(setq ghostel--redraw-timer nil))
(when (and ghostel--term
(ghostel--terminal-live-p)
(not (ghostel--maybe-defer-redraw buffer)))
(ghostel--terminal-live-p))
;; Skip during synchronized output unless forced by scroll/resize.
(unless (and (not ghostel--force-next-redraw)
(ghostel--mode-enabled ghostel--term 2026))
;; Pause line mode if alt-screen just turned on — must run
;; before the line-snapshot block so that snapshot sees the
;; post-pause input mode and skips its own capture.
(ghostel--line-mode-pre-redraw)
(setq ghostel--force-next-redraw nil)
(when-let* ((render-win (ghostel--get-render-window buffer)))
(let* ((all-windows (get-buffer-window-list buffer nil t))
(anchored (cl-remove-if-not #'ghostel--window-anchored-p
all-windows))
;; In line mode the user's in-progress input lives
;; in the buffer past the prompt and is not in
;; libghostty's grid; the renderer would otherwise
;; clobber it. Snapshot the input region (and clear
;; it from the buffer) before the redraw, then
;; restore it after.
(line-snapshot (and (eq ghostel--input-mode 'line)
(ghostel--line-mode-snapshot)))
(inhibit-read-only t)
(let* ((inhibit-read-only t)
(inhibit-redisplay t)
(inhibit-modification-hooks t)
;; Raise GC threshold to defer GC during redraw.
(gc-cons-threshold (min most-positive-fixnum
(* gc-cons-threshold 3)))
(ghostel--query-font-cache (make-hash-table :test 'eq)))
(with-selected-window render-win
;; Line mode snapshots editable input out of the buffer;
;; redraw fully so the prompt row is always rebuilt
;; before restoring input at its fresh marker position.
(ghostel--redraw ghostel--term (eq ghostel--input-mode 'line)))
(ghostel--apply-cursor-style)

(dolist (win anchored) (ghostel--anchor-window win))

(let* ((line-restored
(and line-snapshot
(ghostel--line-mode-restore line-snapshot))))
;; Restore failed (prompt scrolled off / shell
;; integration dropped out): the input was already
;; deleted from the buffer in snapshot — forward it raw
;; so the user does not lose what they typed.
(when (and line-snapshot (not line-restored))
(let ((input (plist-get line-snapshot :input)))
(when (and input (> (length input) 0))
(ghostel--write-pty ghostel--term input))
(message "ghostel: line-mode prompt lost; input forwarded raw")))

(ghostel--schedule-link-detection (ghostel--viewport-start)
(point-max)))
;; Resume line mode if alt-screen just turned off, and
;; update the alt-screen-prev cache for the next cycle.
(ghostel--line-mode-post-redraw))))
(ghostel--detect-password-prompt)))))
(ghostel--run-around-redraw-functions
(lambda ()
(with-selected-window render-win
(ghostel--redraw ghostel--term
(eq ghostel--input-mode 'line)))
t)))))))))

(defun ghostel-force-redraw ()
"Force an immediate terminal redraw, bypassing synchronized-output batching.
Expand Down Expand Up @@ -5126,7 +5152,7 @@ buffer creation time — see `ghostel--buffer-identity'."
`semi-char' needs nothing."
(pcase ghostel-initial-input-mode
('char (ghostel-char-mode))
('line (setq ghostel--pending-initial-line-mode t))))
('line (ghostel--line-mode-defer-initial-entry))))

;;;###autoload
(defun ghostel (&optional arg)
Expand Down
35 changes: 0 additions & 35 deletions test/ghostel-ime-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,6 @@
;; `quail-overlay' is defined by quail.el, which is not loaded in batch.
(defvar quail-overlay)

(ert-deftest ghostel-test-ime-inhibit-hook-defers-redraw ()
"A non-nil `ghostel-inhibit-redraw-functions' reschedules the redraw.
The buffer must not be redrawn while a feature inhibits it."
(ghostel-test--with-compile-buffer buf
(let ((old-timer (run-with-timer 1000 nil #'ignore))
timer-delay timer-repeat timer-fn timer-args)
(setq-local ghostel--redraw-timer old-timer)
(setq-local ghostel--term 'fake-term)
(add-hook 'ghostel-inhibit-redraw-functions
(lambda (buffer)
(should (eq buffer buf))
(should (eq (current-buffer) buf))
t)
nil t)
(cl-letf (((symbol-function 'ghostel--terminal-live-p)
(lambda (&rest _) t))
((symbol-function 'ghostel--redraw)
(lambda (&rest _)
(ert-fail "Inhibited redraw must not call native redraw")))
((symbol-function 'run-with-timer)
(lambda (delay repeat fn &rest args)
(setq timer-delay delay
timer-repeat repeat
timer-fn fn
timer-args args)
'ghostel-test-ime-timer)))
(ghostel--redraw-now buf))
(should (equal timer-delay ghostel-timer-delay))
(should (null timer-repeat))
(should (eq timer-fn #'ghostel--redraw-now))
(should (equal timer-args (list buf)))
(should (eq ghostel--redraw-timer 'ghostel-test-ime-timer))
(when (timerp old-timer)
(cancel-timer old-timer)))))

(ert-deftest ghostel-test-ime-mode-installs-buffer-local-wrapper ()
"`ghostel-ime-mode' installs and removes its buffer-local wiring."
(ghostel-test--with-compile-buffer buf
Expand Down
7 changes: 4 additions & 3 deletions test/ghostel-line-mode-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -2010,8 +2010,8 @@ prompt and engages on the redraw that exposes one."
(setq ghostel-detect-password-prompts nil)
(set-window-buffer (selected-window) buf)
(setq ghostel--process 'fake-proc)
;; Arm exactly as `ghostel--apply-initial-input-mode' would.
(setq ghostel--pending-initial-line-mode t)
(let ((ghostel-initial-input-mode 'line))
(ghostel--apply-initial-input-mode))
(cl-letf (((symbol-function 'process-live-p) (lambda (_p) t))
((symbol-function 'ghostel--write-pty) #'ignore)
((symbol-function 'ghostel--invalidate) #'ignore))
Expand All @@ -2034,7 +2034,8 @@ prompt and engages on the redraw that exposes one."
(setq ghostel-detect-password-prompts nil)
(set-window-buffer (selected-window) buf)
(setq ghostel--process 'fake-proc)
(setq ghostel--pending-initial-line-mode t)
(let ((ghostel-initial-input-mode 'line))
(ghostel--apply-initial-input-mode))
(cl-letf (((symbol-function 'process-live-p) (lambda (_p) t))
((symbol-function 'ghostel--write-pty) #'ignore)
((symbol-function 'ghostel--invalidate) #'ignore))
Expand Down
Loading
Loading