Skip to content

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

Open
dakra wants to merge 3 commits intomainfrom
eat-input-modes
Open

Add eat-style input modes (char, emacs, copy, line, semi-char)#78
dakra wants to merge 3 commits 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 added 3 commits April 15, 2026 18:19
Adds five input modes inspired by eat.el so the user can switch
between a terminal-focused default, a "send everything" char mode,
a read-only but still-live Emacs mode, a frozen copy mode for
precise selection, and a shell-like line mode that buffers input
locally and sends whole lines to the shell.

- `ghostel--input-mode' (semi-char, char, emacs, copy, line) is the
  single source of truth.  `ghostel--copy-mode-active' is removed.
- Keymap is split into a slim `ghostel-mode-map' base (C-c prefix,
  drag-n-drop) and one keymap per mode
  (`ghostel-{semi-char,char,emacs,copy,line}-mode-map').  Helpers
  `ghostel--define-terminal-keys' and `ghostel--define-mouse-keys'
  share the terminal-sending bindings between semi-char and char.
- Emacs mode is unfrozen: the terminal keeps running and redraws
  keep streaming new output into the buffer while it is read-only,
  so `isearch', `occur', `M-x', and the rest of Emacs's vocabulary
  work over a live log.  The delayed-redraw path preserves point
  unconditionally in Emacs mode so navigation is stable.
- Copy mode is refactored to share read-only/cursor/hl-line
  state-helpers (`ghostel--enter-readonly-state' /
  `ghostel--leave-readonly-state') with Emacs mode.  Copy mode
  additionally freezes the redraw timer.
- Line mode (C-c C-l) never forwards keystrokes to the shell: the
  user edits input locally with standard Emacs keys and sends the
  whole line on RET via a single `process-send-string'.  Redraws
  are suppressed while composing so in-progress input is never
  clobbered; output keeps feeding libghostty internally and
  catches up when the user exits line mode.  History ring with
  M-p / M-n, C-c C-c interrupt, C-d EOF on empty input, requires
  OSC 133 shell integration, refuses to enter on the alt screen.
- Prompt navigation (`C-c C-n' / `C-c C-p') now enters Emacs mode
  instead of copy mode so the terminal keeps running while the
  user jumps between prompts.
- `C-c C-l' is reserved for line mode; clear-scrollback moves to
  `C-c M-l'.
- The five check sites that used `ghostel--copy-mode-active' migrate
  to the new predicates `ghostel--buffer-editable-p',
  `ghostel--terminal-live-p', `ghostel--terminal-frozen-p'.
- `ghostel-evil' now checks for semi-char mode explicitly so its
  editing commands don't fire in any of the read-only modes.
- README gains a new "Input modes" section covering all five modes
  with per-mode keymap tables.
- 27 new tests cover mode state and predicates, keymap definitions,
  mode-switch transitions (including copy <-> emacs round-trips),
  prompt navigation, line-mode prompt discovery, send/history/
  interrupt/EOF behavior, and the redraw interaction with emacs
  and line modes.  120 ghostel tests + 30 evil tests all pass.

Closes #40 (line mode landed alongside the other four modes).
…vation

Addresses five issues found while using the new input modes:

1. `M-RET' did not exit char mode.  Graphical Emacs sends M-RET as
   the `<M-return>' symbol, not the `\M-\r' character that the old
   binding used; terminal Emacs sends it as the character.  Bind
   both forms (and keep `C-M-m' as a synonym) in
   `ghostel-char-mode-map' and document the choices.

2. `C-c' still reached Emacs from char mode when global minor mode
   keymaps bound `C-c' as a prefix.  Route char mode through
   `emulation-mode-map-alists' so `ghostel-char-mode-map' overrides
   minor-mode keymaps rather than just the local map.  A buffer-
   local flag `ghostel--char-mode-override-active' gates the
   override and is cleared whenever the mode is left.

3. Emacs mode exited on self-insert and `q'.  Both bindings are
   removed so typing in the read-only buffer signals the normal
   "Buffer is read-only" error and the user must exit explicitly
   via a `C-c' prefix mode-switch command.  Copy mode keeps its
   fast exit.

4. Copy mode always returned to semi-char on exit, dropping the
   user back from wherever they came (e.g. Emacs mode).  Remember
   the previous mode in `ghostel--pre-copy-mode' on entry and
   dispatch to the right mode-entry function on exit.  Copy's
   fast-exit via self-insert only forwards the typed key when the
   target mode accepts terminal input (semi-char or char).  Line
   mode is not a safe restore target (it is stateful), so falling
   back to semi-char in that case.

5. Exiting line mode discarded the user's in-progress input.
   `ghostel--line-mode-teardown' now forwards any pending input
   to the PTY raw (no newline) so the user can keep editing it at
   the shell's own prompt after the mode switch instead of losing
   what they typed.  `ghostel-line-mode-send' and
   `ghostel-line-mode-interrupt' delete the input from the buffer
   first so teardown does not double-send.  Interrupt now also
   exits to semi-char so the new prompt becomes visible.

Tests updated/added:
- `ghostel-test-emacs-mode-no-self-insert-exit' replaces the old
  exit-and-send test, asserting that typing in emacs mode falls
  through to `self-insert-command'.
- `ghostel-test-copy-mode-restores-previous-mode' exercises the
  pre-copy-mode restore for semi-char, char, and Emacs.
- `ghostel-test-line-mode-exit-sends-pending' verifies that
  exiting line mode via a mode switch ships the in-progress input
  back to the shell.
- `ghostel-test-line-mode-interrupt' updated to expect the
  exit-to-semi-char transition.

All 122 ghostel tests, 30 evil tests, and lint pass.
Two UX fixes found after using the modes:

1. Emacs mode now forwards typed chars to the shell instead of
   falling through to `self-insert-command' (which errored because
   the buffer is read-only).  The map remaps self-insert to
   `ghostel--self-insert' and binds RET/TAB/DEL to
   `ghostel--send-event'.  `ghostel--self-insert' just sends the
   char to the PTY — it never touches the buffer — so the buffer
   stays read-only and the user's point stays wherever they
   navigated (the delayed-redraw preserves point in Emacs mode).
   Navigation keys (C-n/C-p/M-x/arrows/isearch/…) still fall
   through to the global map so Emacs mode is still a full-Emacs
   buffer on top of the live terminal.

2. Line mode now protects the scrollback / previous-output region
   with a `read-only' text property so typing in scrollback
   signals `text-read-only'.  `rear-nonsticky' is set on ONLY the
   last char of the protected region (not the whole region), so
   insertion at the input marker — the first typed character —
   does not inherit read-only from the preceding prompt.  If
   rear-nonsticky covers the whole region, Emacs allows insertion
   at any position inside it, which is exactly the bug the user
   hit.  `ghostel--line-mode-teardown' removes the properties
   before the final redraw so the buffer is editable again on
   exit.

Tests updated:

- `ghostel-test-emacs-mode-self-insert-forwards' replaces the old
  "no self-insert exit" test and asserts the remap goes to
  `ghostel--self-insert', RET/TAB/DEL forward to
  `ghostel--send-event', and navigation keys still fall through
  to the global map.
- `ghostel-test-line-mode-scrollback-read-only' covers both sides:
  editing mid-scrollback errors with `text-read-only', editing at
  the marker works normally, and exiting line mode clears the
  property.
- `ghostel-test-line-mode-teardown-on-exit' adds a sanity check
  that the read-only property is gone after teardown.
@dakra dakra force-pushed the eat-input-modes branch from 312a898 to 3345d6f Compare April 15, 2026 16:19
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

1 participant