Skip to content

feat: add -f/--follow to render appended content like tail -f - #992

Open
DanCard wants to merge 1 commit into
charmbracelet:masterfrom
DanCard:follow-mode
Open

feat: add -f/--follow to render appended content like tail -f#992
DanCard wants to merge 1 commit into
charmbracelet:masterfrom
DanCard:follow-mode

Conversation

@DanCard

@DanCard DanCard commented Jul 17, 2026

Copy link
Copy Markdown

What

glow -f file.md renders the file, then appends newly written content to stdout as the file grows — tail -f for markdown. Output is append-only (never repaints), so scrollback survives and piping works.

How

  • Watching: fsnotify on the file's parent directory, so atomic-rename saves from editors don't kill the watch; write bursts are debounced (100ms). Only appended bytes are read, tracked by offset.
  • Block-safe chunking: markdown can't render mid-block, so pending input is flushed at the last blank line outside a fenced code block, with CommonMark-aware fence tracking (backtick/tilde, fence length, ≤3-space indent, backtick info-string rule).
  • Idle flush: a trailing block with no terminating blank line renders after 8s of quiet. If that splits an open code fence, the opening fence line is re-emitted with the next chunk so the remainder still renders as code.
  • Truncation/rewrite: like tail -f, a shrunken or replaced file gets a rendered --- divider and a full re-render below prior output.
  • Non-markdown files: rendered per complete line, wrapped as code blocks.

--follow requires exactly one local file; stdin, URLs, directories, and combining with --pager/--tui produce clear errors. fsnotify was already a dependency.

Testing

  • Unit tests for boundary detection (fences, short closing fences, tilde fences, indented pseudo-fences) and flush behavior (partial block held, open fence held, fence reopen after idle flush, plain-text line flushing).
  • Verified end-to-end by scripting appends against the built binary: partial paragraphs held until terminated, fences held across internal blank lines, rewrite produced divider + fresh render, trailing paragraph appeared only after the idle flush.
  • go build, go vet, go test ./..., and golangci-lint run clean for the new code.

Known limitations

  • Append semantics: mid-file edits fall back to divider + full re-render.
  • Reference-style links and setext headings don't resolve across chunk boundaries.

🤖 Generated with Claude Code

https://claude.ai/code/session_0115ibTkoRG9o6Brf1reA1Jj

Follow a local markdown file and render newly appended content to
stdout as it arrives, without repainting, so scrollback and piping
keep working.

- watch the parent directory via fsnotify (survives atomic saves),
  debounce write bursts, and read only appended bytes
- flush at block-safe boundaries: the last blank line outside a
  fenced code block, with CommonMark-aware fence tracking
- flush a trailing unterminated block after an 8s idle timeout,
  re-opening a split code fence in the next chunk
- on truncation or rewrite, print a divider and re-render the file
- render non-markdown files per complete line as wrapped code blocks
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