Skip to content

feat: interactive table of contents, clickable links, open in $EDITOR - #24

Merged
borghei merged 6 commits into
mainfrom
feat/interaction
Oct 1, 2026
Merged

borghei merged 6 commits into
mainfrom
feat/interaction

Conversation

@borghei

@borghei borghei commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Interactive table of contents. o moves focus into the sidebar (opening it if needed). Move with j/k, jump with Enter, fold subtrees with h/l, filter headings with /. Clicking a row jumps to it, and the wheel over the sidebar scrolls the sidebar. Jumps are recorded in history.
  • Clickable links. A click on a link follows it through the same path as link-hint mode. Dragging still selects and copies.
  • Open in $EDITOR. e suspends the reader, opens the file in $VISUAL or $EDITOR at the line of the current section, then reloads. Editor commands with arguments are split without a shell, and a path starting with - is prefixed with ./.
  • Enter now follows the first followable link anywhere on screen, web and mail included.
  • Fix: a hung-up terminal (closed SSH session, killed tmux) left ink spinning at 100% CPU. A watchdog now exits one second after an unhandled hangup.
  • The file browser uses the same late-reply filter as the reader.

Known limits

  • The editor line is the line of the enclosing heading, because layout records source positions for headings only.
  • A double-click on a word inside a link follows the link instead of selecting the word.

Testing

Unit tests for the TOC model and the editor argv mapping, and pty tests for TOC focus/filter/jump, link click vs drag, and the editor round trip. fmt, clippy and the full suite pass locally. Windows is compile-checked by this PR's CI only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UVcvmQrSXd2eyt32g4ko19

Press o (toc_focus, rebindable) to move keyboard focus into the sidebar,
opening it if it was closed. j/k/arrows move, g/G first/last,
Ctrl+d/Ctrl+u/PageUp/PageDown page, Enter jumps to the heading through
the back/forward history, h/l fold and unfold subheadings in the sidebar,
/ filters headings as you type, Esc/q/o return to the document.

With mouse capture on, a click on a sidebar row jumps to that heading and
the wheel over the sidebar scrolls it instead of the document. The
sidebar now scrolls to keep the current heading in view, and a heading
on the viewport's second row (where n/N and jumps put it) counts as
current. The interaction state lives in a small TocState model with unit
tests.
With mouse capture on, a press and release on the same cell, with no drag
and not part of a double click, on a link follows it through the same
open_link path as link-hint mode: web and mail links open in the browser,
.md files and #anchors open in ink and are recorded in back/forward
history. The link is found from the link_url of the span under the
pointer, measured in display columns. Dragging still selects and copies,
double and triple clicks still select a word or line, and with
--no-mouse the terminal keeps handling clicks itself.
e (edit, rebindable) suspends the TUI through the same restore and setup
functions used at exit and startup, runs $VISUAL, then $EDITOR, then vi
(notepad on Windows) on the current file, and reloads it afterwards,
keeping the reader on the same heading.

The editor command is split shell-style and never run through a shell.
The line comes from the heading the viewport is in (layout records
source positions for headings only), mapped past stripped frontmatter,
and is only passed when the file still has the loaded text at that line:
+N for vi/vim/nvim/nano/emacs/emacsclient/kak/micro, --goto file:N for
code/codium/cursor, file:N for subl/zed/hx, nothing for unknown editors.
A path starting with - or + gets ./ so no editor reads it as an option or
command. Ctrl-C belongs to the editor while it runs. stdin and URL
documents, an editor that cannot start or that exits non-zero, all end
with the TUI restored and a message in the status bar.
Enter was documented as following the first visible link but only looked
at the top six lines of the viewport, and only for .md and #anchor links.
It now takes the first followable link in the whole viewport, web and
mail links included (through the same open_link path as f), and says so
when there is none.
…wser

The file browser read events straight from crossterm, so a background
colour reply that missed the startup query could arrive as key presses
there. It now enters the TUI through the reader's enter_tui, runs the
same discard_late_reply filter, and reads events through the reader's
queue (input::next_event).
When the terminal goes away (a closed SSH session, a killed tmux),
crossterm's event reader loops forever on the dead tty and never returns
to the event loop, so the SIGHUP that came with the hangup was never
acted on: ink kept running and burned a core. A watchdog thread now
restores what it can and exits with 128+signal if a termination signal
stays pending for a second. It stands down while an external editor owns
the terminal, so ink never exits underneath it.
@borghei
borghei merged commit 569bcc7 into main Oct 1, 2026
8 checks passed
@borghei
borghei deleted the feat/interaction branch October 1, 2026 15:59
@borghei borghei mentioned this pull request Oct 1, 2026
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.

1 participant