Skip to content

feat(tui): re-read the document when its file changes - #10

Merged
oetiker merged 1 commit into
oetiker:mainfrom
moetiker:feat/auto-reload
Sep 10, 2026
Merged

feat(tui): re-read the document when its file changes#10
oetiker merged 1 commit into
oetiker:mainfrom
moetiker:feat/auto-reload

Conversation

@moetiker

@moetiker moetiker commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

A pager left open beside an editor now keeps up with what is being written.

What it does

A document read from a file is re-read whenever that file changes on disk. The
reading position is kept, a live search is re-run against the new text, the
contents pane is rebuilt, and a footnote popup closes because the marker it
points at may have moved. On by default; --no-reload, --reload and
reload = false control it. Standard input is watched for nothing, there being
no file.

How

Noticing the change (src/tui/watch.rs). The event loop already wakes every
120 ms to look at the termination flag, so watching is one stat per tick
comparing modification time and length — no new dependency, nothing to port per
platform. A change is remembered when first seen and acted on only when the next
look finds it unchanged, so a half-written save is never parsed; a write that is
still growing moves the stamp again and the wait starts over. A path that
momentarily does not exist — how many editors save — is a save in progress, not
a reason to throw away what is on screen.

Keeping the reader's place. The position survives the way it survives a
resize, through the source offset of the topmost visible text. Unlike a resize,
that offset has to be carried across the edit first: remap_offset compares the
two sources from both ends to find the region that actually changed, so text
inserted above what the reader is looking at no longer pushes them off it. It
is exact for the single edited region a save produces; several edits at once
collapse into the one region spanning them, which is approximate rather than
wrong.

Where the work happens. Reading and parsing stay in term, because the state
machine touches no file (design spec §13) — App::reload is handed a parsed
document. The render cache is keyed on Doc::version, so the new document
invalidates it by construction rather than by anyone remembering to. A file that
cannot be read or is not text is reported in the status bar and leaves the
document on screen alone.

API breaks

  • mdmost::tui::run gained a second parameter, source: Option<&Path>.
  • Config gained a public field, reload: bool, and a method, math_syntax,
    which is now the one place math and math_backslash become a MathSyntax.

Both are recorded in CHANGES.md.

Testing

16 new tests, each written and watched fail before the code existed:

  • the watcher — quiet file, settle, a write still in flight, a file that vanishes;
  • remap_offset — offsets before, after and inside the edit, and identity;
  • App::reload — position kept under an edit above and below it, clamp on a
    shorter document, contents rebuilt, search re-run, popup closed, notice shown;
  • reload_tick — a settled change reaching the document, and an unreadable file
    leaving it alone;
  • configuration — the reload key, its default, and its round trip through S.

cargo test (34 binaries), cargo clippy --all-targets and cargo fmt --check
are clean. Checked end to end in a pseudo terminal as well: the file was
rewritten under a running pager, and the new text and the reloaded notice both
appear in the painted frame.

A pager left open beside an editor now keeps up with what is being written.
The event loop already wakes every 120 ms to look at the termination flag, so
watching is one `stat` per tick against the file's modification time and length
and needs no new dependency. A change is acted on only once a second look finds
it unchanged, so a half-written save is never parsed, and a path that briefly
vanishes -- how many editors save -- is waited out rather than treated as an
empty document.

The reading position survives the way it survives a resize, through the source
offset of the topmost visible text. Unlike a resize, that offset has to be
carried across the edit first: `remap_offset` compares the two sources from both
ends, so text inserted above what the reader is looking at no longer pushes them
off it. A live search is re-run against the new source rather than re-projected,
the contents pane is rebuilt, and the footnote popup closes because the marker
it points at may have moved.

Reading and parsing stay in `term`: the state machine touches no file (design
spec 13), so it is handed a parsed document. A file that cannot be read is
reported in the status bar and leaves the document on screen alone.

On by default, with `--no-reload`, `--reload` and `reload = false` to control it.
Standard input is watched for nothing, there being no file.
@oetiker
oetiker merged commit 73f6956 into oetiker:main Sep 10, 2026
4 checks passed
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.

2 participants