Skip to content

Add eat-style input modes (char, emacs, copy, line, semi-char)#78

Open
dakra wants to merge 1 commit intomainfrom
eat-input-modes
Open

Add eat-style input modes (char, emacs, copy, line, semi-char)#78
dakra wants to merge 1 commit intomainfrom
eat-input-modes

Conversation

@dakra
Copy link
Copy Markdown
Owner

@dakra dakra commented Apr 11, 2026

Summary

Adds five eat.el-inspired input modes for ghostel. Closes #40.

  • semi-char (default): terminal input, C-c prefix reserved
  • char (C-c M-d): all keys to terminal, M-RET exits — for TUI apps that want C-x, M-x, C-h
  • Emacs (C-c C-e): unfrozen — terminal keeps running, buffer is read-only, standard Emacs keys (isearch, occur, M-x, C-SPC+M-w) fall through to the global map. Lets you search across a live log
  • copy (C-c C-t): frozen + read-only + aggressive copy keymap (M-w copies and exits)
  • line (C-c C-l): buffers input locally in Emacs; RET sends the whole line to the shell in one write. Full Emacs editing (yank, transpose, kill-word…) works. Requires OSC 133 shell integration

Mechanics

  • Single state variable ghostel--input-mode; ghostel--copy-mode-active removed
  • Keymap split: slim base ghostel-mode-map (C-c prefix + drag-n-drop) with per-mode child maps
  • New predicates ghostel--buffer-editable-p, ghostel--terminal-live-p, ghostel--terminal-frozen-p — all five check sites that used copy-mode-active migrated
  • Emacs mode reuses the existing inhibit-read-only/inhibit-redisplay bindings around ghostel--redraw, so nothing in the Zig layer changes; the delayed-redraw path now preserves point unconditionally in Emacs mode
  • Line mode suppresses redraws while composing and forces a full catch-up redraw on exit — no save/restore-around-redraw race
  • Prompt nav (C-c C-n/C-c C-p) now auto-enters Emacs mode instead of copy mode so the terminal keeps running during navigation

Base

This PR targets scrollback-in-buffer (PR #73) since it depends on the always-materialized-scrollback model. Once #73 merges to main, this can be rebased onto main.

Test plan

  • make build — native module compiles (no Zig changes, sanity check)
  • make test-all — 120 ghostel tests pass (93 existing + 27 new mode tests)
  • make test-evil — 30 evil-integration tests still pass
  • make lint — package-lint + checkdoc clean
  • Manual smoke test: C-c M-d char mode, C-c C-e Emacs mode + isearch across streaming output, C-c C-t copy mode, C-c C-l line mode with bash + ls -la + RET
  • CI green (all matrix jobs)

@dakra dakra force-pushed the eat-input-modes branch 2 times, most recently from b1e771f to 6d81a44 Compare April 11, 2026 14:02
@dakra dakra force-pushed the scrollback-in-buffer branch from 95cacee to 1a31d37 Compare April 11, 2026 20:23
@dakra dakra force-pushed the eat-input-modes branch from 6d81a44 to fa50ddd Compare April 11, 2026 20:41
@dakra dakra changed the base branch from scrollback-in-buffer to main April 11, 2026 20:41
@dakra dakra force-pushed the eat-input-modes branch from fa50ddd to 312a898 Compare April 14, 2026 14:31
@dakra dakra force-pushed the eat-input-modes branch from 312a898 to 3345d6f Compare April 15, 2026 16:19
@dakra dakra force-pushed the eat-input-modes branch 6 times, most recently from f31dab1 to 9b6240f Compare May 1, 2026 21:26
@krisbalintona
Copy link
Copy Markdown

krisbalintona commented May 1, 2026

FYI I'm trying out this branch and calling ghostel-download-module causes my Emacs to crash. But calling Emacs right after shows no apparent breakage. Example:

Fatal error 11: Segmentation fault
Backtrace:
emacs(+0x1c0fcc) [0x6391290effcc]
emacs(+0x529e8) [0x639128f819e8]
emacs(+0x52f39) [0x639128f81f39]
emacs(+0x52f40) [0x639128f81f40]
emacs(+0x1bf6cc) [0x6391290ee6cc]
/usr/lib/libc.so.6(+0x3e2d0) [0x7759b0cc22d0]
/lib64/ld-linux-x86-64.so.2(+0xabe3) [0x7759b5a24be3]
/lib64/ld-linux-x86-64.so.2(+0xb0fc) [0x7759b5a250fc]
/lib64/ld-linux-x86-64.so.2(+0xba1c) [0x7759b5a25a1c]
/usr/lib/libc.so.6(+0x168e3e) [0x7759b0dece3e]
/usr/lib/libc.so.6(+0x93010) [0x7759b0d17010]
/lib64/ld-linux-x86-64.so.2(_dl_catch_exception+0xa6) [0x7759b5a1c456]
/lib64/ld-linux-x86-64.so.2(+0x25a9) [0x7759b5a1c5a9]
/usr/lib/libc.so.6(+0x92a03) [0x7759b0d16a03]
/usr/lib/libc.so.6(dlsym+0x8e) [0x7759b0d170ae]
emacs(+0x26fa54) [0x63912919ea54]
/home/krisbalintona/.emacs.d/var/eln-cache/31.0.50-ef4348d8/ghostel-d6cb9d6f-e46e862b.eln(F67686f7374656c2d646f776e6c6f61642d6d6f64756c65_ghostel_download_module_0+0x141) [0x77593a8efac1]
emacs(+0x23a6b4) [0x6391291696b4]
emacs(+0x23235b) [0x63912916135b]
emacs(+0x23a6b4) [0x6391291696b4]
emacs(+0x232f60) [0x639129161f60]
/home/krisbalintona/emacs/31.0/bin/../lib/emacs/31.0.50/native-lisp/31.0.50-ef4348d8/preloaded/simple-fab5b0cf-a4ed70fb.eln(F636f6d6d616e642d65786563757465_command_execute_0+0x2a5) [0x7759abb540d5]
emacs(+0x23a6b4) [0x6391291696b4]
/home/krisbalintona/emacs/31.0/bin/../lib/emacs/31.0.50/native-lisp/31.0.50-ef4348d8/preloaded/simple-fab5b0cf-a4ed70fb.eln(F657865637574652d657874656e6465642d636f6d6d616e64_execute_extended_command_0+0x1e1) [0x7759abb52e91]
emacs(+0x23a6b4) [0x6391291696b4]
emacs(+0x23235b) [0x63912916135b]
emacs(+0x23a6b4) [0x6391291696b4]
emacs(+0x23aa80) [0x639129169a80]
emacs(+0x233deb) [0x639129162deb]
/home/krisbalintona/emacs/31.0/bin/../lib/emacs/31.0.50/native-lisp/31.0.50-ef4348d8/preloaded/simple-fab5b0cf-a4ed70fb.eln(F636f6d6d616e642d65786563757465_command_execute_0+0x2a5) [0x7759abb540d5]
emacs(+0x23a6b4) [0x6391291696b4]
emacs(+0x1b2db8) [0x6391290e1db8]
emacs(+0x2353b4) [0x6391291643b4]
emacs(+0x19cab6) [0x6391290cbab6]
emacs(+0x23530e) [0x63912916430e]
emacs(+0x19ca53) [0x6391290cba53]
emacs(+0x1a5242) [0x6391290d4242]
emacs(+0x1a55db) [0x6391290d45db]
emacs(+0x5becd) [0x639128f8aecd]
/usr/lib/libc.so.6(+0x276c1) [0x7759b0cab6c1]
/usr/lib/libc.so.6(__libc_start_main+0x89) [0x7759b0cab7f9]
...
fish: Job 1, 'emacs' terminated by signal SIGSEGV (Address boundary error)

Not sure if this is due to this branch or is generally a bug on main (don't have time to debug or check right now).

@krisbalintona
Copy link
Copy Markdown

krisbalintona commented May 1, 2026

@dakra First impressions:

It would be nice to have a mode line indicator for which input mode you're in, like EAT does. (EAT appends an indicator to the mode line lighter.)

I appreciate the echo area info on how to leave the input mode once you enter it; very useful for newcomers + those without good muscle memory for the package (yet).

Why would one enter copy mode vs emacs mode? Is copy mode meant to be a specialized emacs mode? If so, what benefit does it have over regular emacs mode? (In EAT, I would enter their emacs mode if I wanted to copy anything.)

It isn't immediately obvious what the benefit of line mode is: when should I enter it? (This one is more because I'm ignorant, not because it shouldn't exist.)

Bug in line mode?: Say I am in char mode and type some command without sending it, ls -la. Then I enter line mode and type anything to "complete" the command (or nothing, if I'm happy with it already), then press RET. The command will be sent but visually the command is doubled. That is, after my shell prompt, I'll see ls -lals -la if I went into line mode and just pressed RET. As a user, I would like whatever is shown on that shell prompt line to just be the prompt + the final command I sent in line mode. Not sure if this is possible.

@dakra dakra force-pushed the eat-input-modes branch from b1dfdaa to 98c4ba3 Compare May 2, 2026 21:39
@dakra
Copy link
Copy Markdown
Owner Author

dakra commented May 2, 2026

It would be nice to have a mode line indicator for which input mode you're in, like EAT does. (EAT appends an indicator to the mode line lighter.)

There should be a mode line indicator. (for semi-char I left it blank as a default)

Why would one enter copy mode vs emacs mode? Is copy mode meant to be a specialized emacs mode? If so, what benefit does it have over regular emacs mode? (In EAT, I would enter their emacs mode if I wanted to copy anything.)

copy mode is basically emacs mode, but copy-mode freezes the terminal while emacs-mode is live.. so e.g. if you tail something, copy-mode doesn't move, while in emacs-mode it continues to scroll.

It isn't immediately obvious what the benefit of line mode is: when should I enter it? (This one is more because I'm ignorant, not because it shouldn't exist.)

I'm not an eat user, so I'm good with only semi-char+copy-mode, but many asked for eat line-mode.
There you have your normal Emacs navigation/editing bindings and don't have to use what the shell / readline provides you.

Bug in line mode

Thanks, I'll have a look
EDIT: Should be fixed with the latest commit.

dakra added a commit that referenced this pull request May 2, 2026
Calling module-load on a path whose shared library is already mapped
into the running Emacs makes dyld/ld.so return the existing handle and
resolve emacs_module_init via dlsym on the stale image, which segfaults
(reported in #78).

Skip the second module-load when ghostel-module is already featurep'd
and tell the user to restart Emacs to pick up the new version.
@dakra dakra force-pushed the eat-input-modes branch from 98c4ba3 to f446c9a Compare May 2, 2026 21:59
@dakra
Copy link
Copy Markdown
Owner Author

dakra commented May 2, 2026

FYI I'm trying out this branch and calling ghostel-download-module causes my Emacs to crash

Thanks for reporting. This is fixed on latest main (by just not letting the user re-load a native module which is not supported by Emacs)

dakra added a commit that referenced this pull request May 2, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
dakra added a commit that referenced this pull request May 3, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
@dakra dakra force-pushed the eat-input-modes branch from 34a3b8c to 3674b70 Compare May 3, 2026 21:52
dakra added a commit that referenced this pull request May 4, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
@dakra dakra force-pushed the eat-input-modes branch from 3674b70 to 92330e2 Compare May 4, 2026 05:48
dakra added a commit that referenced this pull request May 4, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
@dakra dakra force-pushed the eat-input-modes branch from 92330e2 to e3921b3 Compare May 4, 2026 12:34
dakra added a commit that referenced this pull request May 4, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
@dakra dakra force-pushed the eat-input-modes branch from d038fb3 to bc29837 Compare May 4, 2026 13:23
dakra added a commit that referenced this pull request May 4, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
@dakra dakra force-pushed the eat-input-modes branch 2 times, most recently from 46aea4d to c17de90 Compare May 4, 2026 22:09
dakra added a commit that referenced this pull request May 4, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
dakra added a commit that referenced this pull request May 5, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
@dakra dakra force-pushed the eat-input-modes branch from 388579b to 3b1a07f Compare May 5, 2026 11:50
dakra added a commit that referenced this pull request May 5, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
@dakra dakra force-pushed the eat-input-modes branch from 3b1a07f to f7a5270 Compare May 5, 2026 20:14
dakra added a commit that referenced this pull request May 6, 2026
When entering line mode at a prompt where input was typed via the PTY
in a previous mode (e.g. `ls -la` in char/semi-char), the renderer's
ghostel-input span gets adopted into the editable buffer — but the
shell's readline still holds those bytes.  On RET the line was sent
verbatim, so the shell concatenated and echoed a duplicated prefix:
`ls -lals -la`.

Capture the adopted character count on entry and erase it via N
backspaces before any path that writes the buffer back to the PTY
(RET, mode teardown forward).  Backspace works under any cooked-mode
line discipline (bash, fish, zsh, vi-mode readline, python REPL)
where a readline-only kill like C-u would not.

Reported on PR #78.
@dakra dakra force-pushed the eat-input-modes branch from f7a5270 to 46d48eb Compare May 6, 2026 12:04
@dakra dakra force-pushed the eat-input-modes branch from 46d48eb to 23a74a1 Compare May 6, 2026 22:15
Inspired by eat.el.  The user can switch between a terminal-focused
default (semi-char), a send-everything mode (char), a live-but-
read-only mode (emacs), a frozen copy mode, and a shell-like mode
(line) that buffers input locally and sends whole lines.

`ghostel--input-mode' is the single source of truth;
`ghostel--copy-mode-active' is gone.  Predicates
`ghostel--buffer-editable-p', `--terminal-live-p',
`--terminal-frozen-p' replace the old boolean check.  `evil-ghostel'
gates on semi-char specifically.

The keymap splits into a slim base (C-c prefix, mouse, drag-n-drop,
clipboard, xterm-paste) and one keymap per mode.  Mode switches
live in the base map so they work from every live mode:

  C-c C-j  semi-char     C-c M-d  char
  C-c C-e  emacs         C-c C-t  copy (toggle)
  C-c C-l  line          M-RET    char-mode escape hatch

`C-c C-l' (was clear-scrollback) becomes line mode; clear-scrollback
moves to `C-c M-l'.  Prompt navigation (`C-c M-n' / `C-c M-p')
auto-enters Emacs mode so the terminal keeps running while the user
jumps between prompts.

Emacs mode unfreezes the terminal: output streams while the buffer
is read-only, so `isearch', `occur', `M-x', `C-SPC' + `M-w', and the
rest of Emacs's vocabulary work over a live log.  Typed keys do *not*
reach the shell — self-insert / RET / TAB / DEL fall through to the
buffer's `text-read-only' signal, so reading scrollback can't
accidentally fire commands at the prompt.  Pasting via
`ghostel-yank' is preserved as a deliberate, explicit action and
snaps the window back to the live cursor before sending.

Char mode routes through `emulation-mode-map-alists' so a global
minor-mode prefix on `C-c' can't steal the key first.
`M-RET' / `<M-return>' / `C-M-m' all switch back to semi-char to
cover GUI and TTY Emacs.

Copy mode and Emacs mode share a unified read-only framework
(`ghostel-readonly-mode-map', `ghostel-readonly-*' commands,
`ghostel-readonly-fast-exit' defcustom).  Copy mode freezes the
redraw timer; Emacs mode keeps it live.  Both remember the previous
mode in `ghostel--pre-readonly-mode' so exit returns the user to
wherever they came from rather than always semi-char.

Line mode is live — output keeps streaming, but each redraw
snapshots the in-progress input (text + point/mark offsets), clears
the prompt row, and re-inserts the input at the new prompt-end
after the renderer rewrites the viewport.  `ghostel-full-redraw' is
forced buffer-locally so the dirty-row diff path always rebuilds
the prompt.  The input region is bounded by
`ghostel--line-input-start' and `--line-input-end' so status bars
or any non-blank content drawn below the prompt survive entry and
per-redraw snapshot/restore; the trailing-blank trim is gated to
only delete pure whitespace.  Pre-existing `ghostel-input' cells
are adopted as the initial input on entry, so typing in semi-char
and switching mid-edit keeps the typed chars — and the adopted
character count is erased from the shell's readline buffer with N
backspaces before any send path so RET doesn't duplicate the
prefix.

Prompt discovery uses the terminal cursor as the source of truth
(via the new `ghostel--cursor-row-char-offset' Zig binding, which
walks the cursor row's cells so wide/box-drawing glyphs map
correctly across platforms).  OSC 133 markers refine the location
when the cursor's own row carries `ghostel-prompt' chars.  This
makes line mode work in REPLs without shell integration (python3,
irb, sqlite3 launched under bash) — it no longer lands on the
outer bash prompt.

History ring (`M-p' / `M-n'), `C-c C-c' interrupt, `C-d' EOF on
empty input.  Send and interrupt stay in line mode — the next
redraw moves the marker to the next prompt and the user keeps
editing.  If the prompt cannot be located after a redraw (shell
integration dropped out and the cursor is unavailable), the
captured input forwards raw rather than being lost.  Refuses to
enter on the alt screen.  Scrollback is protected with a
`read-only' text property, with `rear-nonsticky' on the boundary
char covering all properties so text typed after a colored prompt
(e.g. python's purple-bold `>>> ') doesn't inherit the prompt's
face.  Restored input is tagged `ghostel-input' so URL detection
leaves it alone.  Self-inserting a key while point sits in the
read-only scrollback snaps to `ghostel--line-input-end' first and
inserts there, mirroring the "type to dismiss" feel of copy/Emacs
mode without leaving line mode.

`C-a' is smart in both line and Emacs modes:
`ghostel-beginning-of-input-or-line' jumps to the start of input on
a prompt row and falls through to `move-beginning-of-line'
otherwise, so it works correctly from scrollback too.

RET follows links in copy, Emacs, and line modes.  In line mode,
`ghostel-line-mode-send-or-open-link' opens the link at point if
there is one and otherwise falls through to send.  The text-property
`ghostel-link-map' still owns mouse-1/mouse-2.

TAB runs `completion-at-point' against the input region (filenames,
env vars, executables on PATH, pcomplete extensions).  When the
`bash-completion' package is installed,
`bash-completion-capf-nonexclusive' is layered on by default
(`ghostel-line-mode-use-bash-completion'), so `complete -F _git git'
and friends work — TAB after `git checkout ' lists branch names.
Optional `ghostel-line-mode-bash-completion-prespawn' amortises the
first-call cost at mode-entry time.

README gains an "Input modes" section.  Tests cover state,
predicates, keymap definitions, transitions, prompt-navigation
auto-Emacs, line-mode prompt discovery (cursor and OSC 133),
cell-walking offset, send, history, interrupt, EOF, snapshot/
restore across realistic post-RET sequences, end-marker bounding,
existing-input adoption, readline-prefix erase, status-bar
preservation, snap-on-type, snap-on-self-insert from scrollback,
smart-C-a, RET-follows-link in all read-only modes, and bash-
completion integration.

Closes #40.
@dakra dakra force-pushed the eat-input-modes branch from 23a74a1 to 18e6500 Compare May 6, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Project plans in relation to eat.el-like input modes

2 participants