Skip to content

Latest commit

 

History

History
174 lines (133 loc) · 13 KB

embedding.md

File metadata and controls

174 lines (133 loc) · 13 KB

Embedding the diff engine

diffs_rs::engine::Engine owns one review, its view state, source storage, and background workers. The diffs binary and examples/embedded_viewer.rs use this same implementation. The API is pre-1.0 and can change between minor versions.

Dependency setup

The host must use Ratatui 0.30.

[dependencies]
diffs-rs = { version = "0.1", default-features = false }
ratatui = { version = "0.30.2", default-features = false, features = ["crossterm"] }

The default dev-tty feature makes Crossterm read keyboard input from /dev/tty, which the standalone viewer needs when a patch arrives on stdin. Crossterm features are shared by the whole build, so a host that reads its own input should use default-features = false. Enable the clipboard feature only if the host wants the native macOS clipboard. Diagnostic crate features are not required for embedding.

The patched syntax engine comes from the forked crates diffs-syntect, diffs-fancy-regex and diffs-two-face. They are ordinary dependencies of diffs-rs, so the host needs no [patch] section. Their library names stay syntect, fancy_regex and two_face. A host that also depends on upstream Syntect gets a second, separate copy whose types do not interoperate with the fork's. Do not substitute upstream Syntect, Fancy Regex or two-face to make dependency resolution succeed. The Git patch parser is the stock libgit2-sys crate with its bundled libgit2. The build fails if LIBGIT2_NO_VENDOR asks it to link a system libgit2.

Ownership

The host owns the terminal, raw mode, alternate screen, mouse capture, signal handling, keyboard reader, outer layout, and application quit behavior. The engine never reads the global keyboard, changes terminal modes, writes terminal output directly, or exits the process. Only the host should read Crossterm input in the process.

Open with Engine::open(input, options, wake_sender). Input accepts a patch path, an owned file stream with a label, or the existing LocalInput file-pair or pinned-Git comparison. Opening starts background work. Inspect status() as input arrives; successful open() does not mean the input has been parsed or verified. Commands issued before the first file arrives return InputEffect::Ignored.

Input::Stream reads from the file's current offset and takes exclusive control of its shared read cursor and status flags until close. Caller-held duplicates must not read, seek, or change those flags concurrently. The patch reader temporarily enables nonblocking input and restores the original nonblocking bit when it finishes or closes. It supports cancellation while waiting for a FIFO writer, an idle stream, or an incomplete line. The safe descriptor-set implementation rejects descriptors at or above FD_SETSIZE before changing flags. This is a Unix implementation. macOS arm64 is the most tested system; Linux runs in CI.

Host loop

Use a bounded std::sync::mpsc::sync_channel(1) for wake notifications. Each notification is a hint that work may be ready, not one notification per reply. The example implements this order:

  1. Clear the pending wake token before draining work.
  2. Call engine.poll(). Each call drains a bounded number of replies so the host can handle input.
  3. Redraw when engine.needs_draw() or the host's layout/focus has changed.
  4. Call engine.draw(frame, viewer_area) inside the host's Ratatui draw closure. Propagate both backend and engine draw errors. After successful terminal output, call engine.after_draw() before processing further input or replies. This starts visible work after the text frame has been written and completes redraw bookkeeping.
  5. Handle host commands first, then route viewer events or commands.
  6. Wait for host input, a wake notification, or a bounded periodic tick.

The example uses a 100 ms maximum idle tick because progressive file publication also has a redraw deadline. A host that waits only for wake tokens can miss that deadline after clearing a token early. Continue polling and drawing when the viewer lacks keyboard focus; otherwise colors and source readiness cannot become visible.

The draw rectangle is intersected with the frame. Engine drawing does not change cells outside that intersection. The supplied rectangle controls reflow. Terminal resize events request a redraw but do not replace the pane dimensions with the full terminal dimensions. Supply the current rectangle on every draw, including when a pane moves without changing size.

Commands and selection

handle_event(event, focused) applies the viewer's default bindings. Unfocused key events are ignored. Mouse coordinates are absolute terminal cells; events outside the last drawn pane are ignored. The host decides whether a click changes outer focus and can withhold pointer events from the viewer. execute(Command) bypasses key mapping for host-owned controls. The default help/footer describe the standalone bindings; hosts that remap them must provide corresponding user guidance.

selected_source() returns an optional SourceLocation:

  • review identifies this open occurrence within the process. It changes on replacement and is not a persistent revision identifier.
  • file is the stable file occurrence within that review. file(index) exposes its paths, status and change counts.
  • side identifies the selected original side.
  • old_line and new_line are one-based original source coordinates of the selected record. A replacement's old and new records remain separate. A shared context record can carry both numbers.

Headers, gap controls, metadata-only rows, absent replacement halves and wrapped alignment padding return no source location. Displayed escapes, tab cells and wrapped row offsets are not raw byte offsets. This API does not expose byte-column selections. Use the review identity with stored host selections so an old selection cannot attach to a new review with the same path or file ordinal.

execute(Command::SelectLines) begins or fixes a source-line range. ClearSelection clears it. Both commands are ignored while help or the file tree owns focus, or the diff pane is empty. selection() returns its SourceRange; selection_extending() reports whether navigation extends it. The range stores coordinates, not source ownership. Reflow preserves it. Explicit side changes, another file, and review replacement clear it. Withdrawal of an affected checked source also clears it.

The default mouse route supports Shift-click within the same file and side. The host still controls event routing and keyboard bindings. The standalone maps V and Escape to these commands. handle_event alone does not add those keyboard bindings. Headers and padding never become endpoints. Ranges can span unavailable patch-only gaps; source_read rejects those gaps during streaming.

Source reading and search

source_read(SourceRange) prepares an original-side range with inclusive source-line coordinates. Its write_to operation streams original bytes to a caller-owned writer with cancellation checks. SourceRead::check checks review and checked-source validity without reading bytes. The caller checks it again before publishing prepared output. Checked full sources can supply hidden context. Patch-only ranges reject missing context instead of joining separated source fragments. The range preserves tabs, invalid bytes, CRLF/LF, and final-newline state. Display sanitization does not alter the exported bytes.

search_read(query) captures diff text and the current source origin after indexing completes. The scope contains supplied patch records and explicitly expanded context, not hidden complete files. The caller runs SearchRead::find outside its input thread and owns cancellation, reply acceptance, and worker shutdown. SearchRead::check checks scope validity without scanning source text. show_search_match checks review/scope identity, reveals the source position, and marks the current match. clear_search_match removes that emphasis without changing the reader's position. The caller must reject superseded replies before show_search_match, even if those replies have a valid review identity.

The engine supplies no search prompt, extra keyboard reader, clipboard action, or search-worker loop. draw_with_controls optionally accepts borrowed ReadingControls for caller hints, help, and status. Input failures and source progress retain their status alongside these notices. The existing draw method does not add standalone search bindings. The host must cancel and join its reading jobs before replacement or close, then release retained source readers. The standalone owns its save prompt, private output, no-clobber publication, and cleanup. The engine does not create export destinations. The standalone also owns clipboard preparation, its helper process, and delivery acceptance. Its optional clipboard feature enables macOS plain-text publication. Engine-only consumers can disable default features to omit that dependency. The engine does not launch clipboard helpers or handle the standalone y key.

Failure and shutdown

status().loading refers to patch indexing. coloring and preparing_source refer to the current visible rows, not the whole review. An empty or too-small pane can have neither visible flag set without implying that all source has been colored. source_failed records an unavailable checked source during this review; existing patch content remains readable with its declared limited context.

An input error appears in status().error. Successfully published files remain readable, and the host can replace the failed review. An unexpected syntax-worker failure, source read failure, or rendering failure returns an error from the corresponding engine call. The host should stop using that review, report the error, and call close() before opening another.

replace(input) closes and joins the old review before starting new workers and channels. It preserves current layout, theme, wrapping, line-number, and sidebar preferences, but not selection or expanded context from the previous review. No old-review replies can enter the new channels.

close() cancels and joins both workers, releases review-owned sources and view state, and is idempotent. Previously delivered input-operation errors do not make a successful cleanup fail. Cleanup failures and worker panics are reported, including simultaneous cleanup errors. Call it explicitly so the host can report those errors. Drop is best-effort cleanup, not an error-reporting channel. The host must also preserve cleanup errors when its own event loop has already failed; the example demonstrates this.

Cancellation cannot interrupt one in-progress syntax-parser call or an operating-system filesystem call. No universal shutdown-latency bound is promised. The engine does not detach unfinished workers or leave old reviews running after a successful close.

Closing a review does not promise that process RSS returns to its opening value: the macOS allocator can keep freed regions resident. After a large patch, closed RSS reached about 590 MiB even though live heap had returned to its earlier size. On a three-cycle schedule of file-pair, Git, and small-patch reviews, the example host stayed under 34 MiB held RSS, and closed RSS rose 0.41 MiB across the cycles.

Try the host example

From a clone of this repository:

cargo run --release --locked --example embedded_viewer -- fixtures/review.patch fixtures/position/review.patch

Mix patch paths with repeatable --files OLD NEW and --git REPO BASE HEAD groups. Reviews open in argument order:

cargo run --release --locked --example embedded_viewer -- fixtures/review.patch \
  --files fixtures/position/old/review.ts fixtures/position/new/review.ts \
  --git /absolute/path/to/repository BASE_COMMIT_ID HEAD_COMMIT_ID

Use full commit IDs to keep Git inputs fixed across repeated opens. A symbolic revision resolves again on each open. File pairs capture fresh bytes on each open; editing the live files does not change an already captured review. Arguments are validated before acquiring the terminal.

The left pane belongs to the host. Tab switches outer focus; r opens the next review, c closes the review without exiting, o reopens it, and q exits the host. These are example-host controls, not additional viewer bindings. The example uses the current terminal and does not open windows or change preferences. State: source unavailable distinguishes degraded full-source preparation from a ready review. Published patch content remains readable, and the host can replace the review. Unexpected worker/storage errors and failed cleanup terminate the example with an error; they are not treated as recoverable input errors.

The integration checks cover bounded cells and styles, source coordinates, focus, checked context, replacement, FIFO cancellation, and terminal restoration. A separate Cargo-root build checks the dependency recipe; same-package examples alone do not prove dependency integration. These checks establish emitted cells and lifecycle behavior. They do not establish physical screen-paint timing or terminal font behavior.