Skip to content
Closed
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
69 changes: 68 additions & 1 deletion lisp/ghostel.el
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,13 @@ Matches Ghostty 1.2.0's `bold-color' configuration."
(defvar-local ghostel--cursor-char-pos nil
"The position of the terminal cursor in the buffer.")

(defvar-local ghostel--cursor-visible nil
"Last VISIBLE value passed to `ghostel--set-cursor-style'.
Mirrors the terminal's DECTCEM (show/hide cursor) state. Consumed
by `ghostel--maybe-suppress-cursor' so the redisplay-time hook can
keep `cursor-type' off when the terminal has hidden the cursor,
even if something else has reset `cursor-type' in the meantime.")

(defvar-local ghostel--rendered-font nil
"The font last used for rendering. Internally used by native code.")

Expand Down Expand Up @@ -5294,13 +5301,72 @@ Only acts when the buffer has not been manually renamed by the user."
(when ghostel-set-title-function
(funcall ghostel-set-title-function title)))

(defun ghostel--cell-face-background (pos)
"Return the effective background color string of the face at POS.
Walks the `face' text property in its various shapes (symbol,
anonymous plist, or list of either) and returns the first
explicit background found, or nil."
(cl-labels
((bg-of (f)
(cond
((null f) nil)
((symbolp f)
(let ((bg (face-attribute f :background nil t)))
(and (stringp bg) bg)))
((and (listp f) (keywordp (car f)))
(plist-get f :background))
((listp f) (seq-some #'bg-of f)))))
(bg-of (get-text-property pos 'face))))

(defun ghostel--cursor-cell-conflicts-p ()
"Return non-nil when the cursor cell's face background equals the cursor color.
Emacs's GUI ports (X11, mac-port, w32, pgtk) share a `set_cursor_gc'
distinct-swap fallback that silently swaps the cursor's fg/bg with
the cell's fg/bg whenever they match (e.g. xterm.c:8017,
macterm.c:1042). The swap result coincides with the surrounding
non-inverse cells, so the cursor becomes visually indistinguishable
from normal text. When this returns non-nil, callers suppress
`cursor-type' so the cell's own inverse video face represents the
cursor position."
(when-let* ((pos ghostel--cursor-char-pos)
((< pos (point-max)))
(cell-bg (ghostel--cell-face-background pos))
(cursor-color (frame-parameter nil 'cursor-color))
(cell-rgb (color-values cell-bg))
(cursor-rgb (color-values cursor-color)))
(equal cell-rgb cursor-rgb)))

(defun ghostel--maybe-suppress-cursor (&optional _window)
"Force `cursor-type' to nil when the cursor should be hidden.

Two conditions trigger suppression:
- The terminal sent DECTCEM hide (\\='\\=e[?25l), captured in
`ghostel--cursor-visible'. Without this branch, TUI menus that
hide the cursor would still show Emacs's box cursor on top of
whatever cell is at the saved cursor position.
- The cursor cell's face background collides with the cursor color;
see `ghostel--cursor-cell-conflicts-p' for why that hides the cursor.

Installed as a buffer-local `pre-redisplay-functions' entry because
`ghostel--set-cursor-style' alone is not enough: `cursor-type' is
restored to its default between module render passes, and this hook
reapplies the suppression just before each redisplay."
(when (and (derived-mode-p 'ghostel-mode)
cursor-type
(or (not ghostel--cursor-visible)
(ghostel--cursor-cell-conflicts-p)))
(setq cursor-type nil)))

(defun ghostel--set-cursor-style (style visible)
"Set the cursor style based on terminal state.
STYLE is one of: 0=bar, 1=block, 2=underline, 3=hollow-block.
VISIBLE is t or nil.
Skipped in read-only input modes (copy, Emacs, line) where the
user-facing cursor is managed by Emacs for navigation, or when
`ghostel-ignore-cursor-change' is non-nil."
`ghostel-ignore-cursor-change' is non-nil.
Face-background conflict suppression is handled separately by
`ghostel--maybe-suppress-cursor', which runs on each redisplay."
(setq ghostel--cursor-visible (and visible t))
(when (and (ghostel--buffer-editable-p)
(not ghostel-ignore-cursor-change))
(setq cursor-type
Expand Down Expand Up @@ -6760,6 +6826,7 @@ output is arriving."
(add-hook 'window-buffer-change-functions #'ghostel--focus-change)
(add-hook 'window-buffer-change-functions #'ghostel--reshow-snap nil t)
(add-hook 'window-buffer-change-functions #'ghostel--sync-tty-composition nil t)
(add-hook 'pre-redisplay-functions #'ghostel--maybe-suppress-cursor nil t)
(ghostel--suppress-interfering-modes)
(ghostel-imenu-setup)
(setq ghostel--scroll-intercept-active t)
Expand Down
66 changes: 65 additions & 1 deletion test/ghostel-modes-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
;;; Commentary:

;; Input mode state + char/emacs/copy mode transitions, fake cursor,
;; copy-mode cursor + hl-line.
;; copy-mode cursor + hl-line, cursor suppression on bg conflict.

;;; Code:

Expand Down Expand Up @@ -712,5 +712,69 @@ Otherwise the window stays where the user navigated."
(should-not buffer-read-only))))
(kill-buffer buf))))

(ert-deftest ghostel-test-cell-face-background-shapes ()
"`ghostel--cell-face-background' resolves the bg across face-property shapes."
(defface ghostel-test--no-bg '((t :foreground "#00ff00"))
"Test face without a background.")
(defface ghostel-test--with-bg '((t :background "#0000ff"))
"Test face with a background.")
(defface ghostel-test--inherits '((t :inherit ghostel-test--with-bg))
"Test face inheriting a background.")
(with-temp-buffer
;; Anonymous plist with an explicit background.
(insert (propertize "x" 'face '(:background "#ff0000")))
(should (equal "#ff0000" (ghostel--cell-face-background (point-min))))
;; Plist without :background resolves to nil.
(erase-buffer)
(insert (propertize "y" 'face '(:foreground "#00ff00")))
(should (null (ghostel--cell-face-background (point-min))))
;; No face property at all.
(erase-buffer)
(insert "z")
(should (null (ghostel--cell-face-background (point-min))))
;; Named face with an explicit background.
(erase-buffer)
(insert (propertize "a" 'face 'ghostel-test--with-bg))
(should (equal "#0000ff" (ghostel--cell-face-background (point-min))))
;; List of faces: a leading face without a background must not mask the
;; later face that supplies one.
(erase-buffer)
(insert (propertize "b" 'face '(ghostel-test--no-bg ghostel-test--with-bg)))
(should (equal "#0000ff" (ghostel--cell-face-background (point-min))))
;; The :inherit chain is followed when the face has no direct background.
(erase-buffer)
(insert (propertize "c" 'face 'ghostel-test--inherits))
(should (equal "#0000ff" (ghostel--cell-face-background (point-min))))))

(ert-deftest ghostel-test-cursor-cell-conflicts ()
"`ghostel--cursor-cell-conflicts-p' compares cursor/cell colors by value.
Stub `color-values' and `frame-parameter' so the result is independent
of the display (color names do not resolve on a display-less batch run)."
(with-temp-buffer
(insert (propertize "x" 'face '(:background "#ffffff")))
(let ((cursor "white"))
(cl-letf (((symbol-function 'color-values)
(lambda (c)
(pcase (downcase c)
((or "white" "#ffffff") '(65535 65535 65535))
((or "black" "#000000") '(0 0 0))
(_ nil))))
((symbol-function 'frame-parameter)
(lambda (_frame param)
(when (eq param 'cursor-color) cursor))))
(setq-local ghostel--cursor-char-pos (point-min))
;; Named "white" cursor vs "#ffffff" cell — same color, conflict.
(should (ghostel--cursor-cell-conflicts-p))
;; Distinct colors — no conflict.
(setq cursor "black")
(should-not (ghostel--cursor-cell-conflicts-p))
;; Cursor position at point-max is out of range — no conflict.
(setq cursor "white")
(setq-local ghostel--cursor-char-pos (point-max))
(should-not (ghostel--cursor-cell-conflicts-p))
;; No saved cursor position — no conflict.
(setq-local ghostel--cursor-char-pos nil)
(should-not (ghostel--cursor-cell-conflicts-p))))))

(provide 'ghostel-modes-test)
;;; ghostel-modes-test.el ends here