feat: configure interchangeable top and bottom bars
This commit is contained in:
@@ -9,7 +9,7 @@ Contributor-facing module boundaries and implementation contracts. Project termi
|
||||
One editor crate; the workspace also includes its patched Crossterm dependency. Terminal-coordinate types remain outside the editing backend:
|
||||
|
||||
- `editor`: owns shared buffer lifecycle/text/saved state/undo history and independent tab/pane layouts, focused views, modes, selections, searches, explorers, and comparisons; session-wide clipboard policy, independent registers and owned-system fragment metadata, semantic native repeat, action metadata, command-palette and file-picker focus/dispatch, key notation, key-sequence/count prefixes and binding resolution, modal paste policy, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
|
||||
- `chrome`: owns validated tabline/statusline anchor groups, segment title templates and padding/edge options, builtin defaults, and named-value validation. It has no terminal geometry or render callbacks.
|
||||
- `chrome`: owns validated top/bottom bar anchor groups, segment title templates and padding/edge options, builtin defaults, and named-value validation. Both bars accept the same segments and layout options. It has no terminal geometry or render callbacks.
|
||||
- `files`: owns the gitignore-aware directory/file catalog, authoritative OS paths, compact tree projection, query filtering, browse/search focus, expansion/selection restoration, navigation, and scan/error state shared by floating and docked presentations.
|
||||
Virtual file leaves retain their original catalog input index for caller-owned annotations; scanned entries and directories remain unannotated.
|
||||
- `diff`: owns searchable Before/After selection, category, focus and explorer visibility, request freshness, read-only comparison DTOs, aligned rows with cached grapheme-level change ranges, and shared vertical/independent horizontal offsets. Changed paths form a virtual `files::FilePicker` catalog, reusing the normal tree projection, filtering, and navigation without scanning disk. It has no subprocess or terminal dependencies.
|
||||
@@ -39,7 +39,7 @@ Frontends inspect `line_operator_pending()` for the optional Normal `d`/`c` pref
|
||||
Editor-owned jump history is a bounded 100-location `VecDeque` of buffer IDs and grapheme cursors plus a current index, with no copied paths/text. Successful high-level navigation records departure/destination once; traversal uses unrecorded buffer activation and position placement, skips closed/clamped-duplicate locations, and truncates forward entries only on a distinct new jump. Session synchronization permanently invalidates pending LSP position requests after their source buffer/revision/caret/mode is left, so returning through history cannot revive an old definition reply.
|
||||
Frontends inspect `buffer_id()`, `buffer_count()`, `has_buffer(id)`, and the ascending-ID `buffers()` iterator of borrowed path/dirty/active metadata. Existing text, cursor, selection, search, and mode methods describe only the active buffer.
|
||||
Frontends inspect `tab_id()` for stable identity, `tab_index()` for display order, `tab_count()`, and `tab_buffer(index)` for borrowed title metadata. `pane_layout()` describes only the active tab; globally unique pane IDs keep inactive tabs' viewports alive. Documents remain unique across all tabs, while independent pane/buffer views are parked and restored without copying text.
|
||||
Frontends borrow `tabline_config()` and `statusline_config()` for validated ordered left/center/right segment groups and effective segment styles, and `label(name)` for session-wide custom values. The TUI owns terminal-cell widths, full-row center anchoring, clipping, and accent styling. Label effects carry only a name and plain text; they are global presentation updates, not buffer/revision-bound results or render callbacks.
|
||||
Frontends borrow `top_bar_config()` and `bottom_bar_config()` for validated ordered left/center/right segment groups and effective segment styles, and `label(name)` for session-wide custom values. The TUI owns terminal-cell widths, full-row center anchoring, clipping, and accent styling. Label effects carry only a name and plain text; they are global presentation updates, not buffer/revision-bound results or render callbacks.
|
||||
|
||||
Frontends inspect `file_picker()` for query, presentation, view, explorer-local INSERT/filter subfocus, selection, and errors, and iterate borrowed `FileRow` metadata (actual path, display label, directory flag, visible depth, expansion). Dock side is a logical preference. They allocate rectangles, add folder suffixes/colors, and render; hierarchy, filtering, parent/child navigation, focus transitions, and selected-file opening remain backend-owned.
|
||||
Frontends inspect `diagnostics()` and `diagnostic_at_cursor()` for validated active-buffer annotations. Diagnostics are owned by core LSP or a plugin and buffer revision; text changes clear them. Native server retirement removes only that server's contribution, preserving sibling diagnostics. The TUI underlines annotated text, colors affected gutter numbers by severity, and displays the primary caret's diagnostic message without replacing search/selection backgrounds.
|
||||
@@ -58,7 +58,7 @@ Redraws stage Ratatui's buffer diff, cursor placement, and DEC synchronized-upda
|
||||
|
||||
`vendor/crossterm` retains Crossterm 0.29.0's upstream source and MIT license, with a small Unix decoder patch exposing `Event::ColorSchemeChanged` for `CSI ? 997 ; 1/2 n`. The report signals a palette refresh, not a choice of hard-coded light/dark colors. Valid fragmented reports and malformed color reports leave following keys intact. Cargo patches both direct and Ratatui dependency paths to this one copy; remove the patch/workspace member once upstream supports these notifications.
|
||||
|
||||
The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, a one-row statusline, and a one-row prompt/message area. Multiple tabs or visible custom top-line segments reserve a tabline row; overflow keeps the active tab title visible. Each chrome row renders configured ordered left/center/right groups, with center anchored to the full row rather than the remaining gap. Padding is inside segment styling, while leading/trailing edge glyphs are separate terminal-cell-width decorations. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. Below three workspace rows the statusline is hidden; at one workspace row the buffer remains visible unless an active command/search prompt takes priority. Zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned.
|
||||
The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, optional one-row top and bottom bars, and a separate prompt/message area. Both bars derive visibility from their configured content before allocating rows: empty content takes no row, and tabs-only content is hidden with a single tab in either position. Both bars render across ordinary, split, diff, and selector views. Each bar renders configured ordered left/center/right groups, with center anchored to the full row rather than the remaining gap. Overflow keeps the active tab title visible wherever tabs appear. Padding is inside segment styling, while leading/trailing edge glyphs are separate terminal-cell-width decorations. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. An active command/search prompt takes priority in minimal height; zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned.
|
||||
|
||||
Mouse wheel input uses the last rendered document/panel rectangles for hit-testing. Document offsets and explorer scroll state remain frontend-owned; three-row wheel steps never become document cursor motions. Manual document views persist until logical caret-reveal, caret/selection, mode, revision, or buffer changes; offscreen primary and browsing-selection cursors are not placed. Hover wheel input reuses the existing editor-owned popup scrolling.
|
||||
|
||||
|
||||
+17
-11
@@ -44,13 +44,23 @@ Plugin paths resolve relative to the config file, not the shell's working direct
|
||||
|
||||
`config.diff_context_lines = 10` sets how many unchanged aligned lines appear above and below each diff change; it defaults to `10` and accepts nonnegative integers, including `0` for changes only. `config.diff_expand_lines = 15` sets how many additional lines each directional expansion reveals per keypress; it defaults to `15` and accepts positive integers. Both are bounded by the platform's Rust `usize` maximum (`18446744073709551615` on 64-bit systems). Floats, strings, booleans, negative or oversized values, and a zero expansion step reject the whole Ruby configuration.
|
||||
|
||||
### Tabline and statusline segments
|
||||
### Top and bottom bars
|
||||
|
||||
`config.tabline` and `config.statusline` each accept ordered `left`, `center`, and `right` arrays of segment names. Left and right groups anchor to their respective edges; the center group is centered on the whole row, not on the gap remaining between the other groups. Array order is display order within each group. Omitted groups and segment options retain defaults; an empty array removes that group.
|
||||
`config.bar.top` and `config.bar.bottom` each accept ordered `left`, `center`, and `right` arrays of segment names. Both bars accept every builtin and custom segment, with the same anchors, title templates, padding, styled edges, and collision handling. Left and right groups anchor to their respective edges; the center group is centered on the whole row, not on the gap remaining between the other groups. Array order is display order within each group. Omitted groups and segment options retain that bar's defaults; an empty array removes that group.
|
||||
|
||||
Use the two setters independently; a combined `config.bar = { ... }` assignment is not supported.
|
||||
|
||||
Defaults are unchanged: the top has `left: [:tabs]`; the bottom has `left: [:mode, :pending, :buffer, :diagnostics, :path, :selections, :count, :language]` and `right: [:position]`; other groups are empty. Set the bars independently, as in this swapped layout with status information above and tabs below:
|
||||
|
||||
```ruby
|
||||
Vamp.configure do |config|
|
||||
config.tabline = {
|
||||
config.bar.top = {
|
||||
left: [:mode, :path, :language, :diagnostics, :selections, :count, :pending, :buffer],
|
||||
center: [], right: [:position],
|
||||
mode: { padding: 1, leading_edge: "", trailing_edge: "▓▒░" },
|
||||
position: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
||||
}
|
||||
config.bar.bottom = {
|
||||
left: [:tabs], center: [], right: [:battery],
|
||||
tabs: {
|
||||
title: "{index} {name}{modified}", padding: 1,
|
||||
@@ -58,23 +68,19 @@ Vamp.configure do |config|
|
||||
},
|
||||
battery: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
||||
}
|
||||
config.statusline = {
|
||||
left: [:mode, :path, :language, :diagnostics, :selections, :count, :pending, :buffer],
|
||||
center: [], right: [:position],
|
||||
mode: { padding: 1, leading_edge: "", trailing_edge: "▓▒░" },
|
||||
position: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
||||
}
|
||||
config.labels = { battery: "bat 38%" }
|
||||
end
|
||||
```
|
||||
|
||||
The top line's builtin is `:tabs`; the bottom line's builtins are `:mode`, `:path`, `:position`, `:language`, `:diagnostics`, `:selections`, `:count`, `:pending`, and `:buffer`. They provide the mode, file path/dirty state, caret position, detected language, diagnostics, selection count, numeric prefix, pending key sequence, and buffer identity/count. Using a builtin on the wrong line rejects configuration. Defaults retain the existing information and accent styling: the top has `left: [:tabs]`; the bottom has `left: [:mode, :pending, :buffer, :diagnostics, :path, :selections, :count, :language]` and `right: [:position]`; other groups are empty.
|
||||
Builtins are `:tabs`, `:mode`, `:path`, `:position`, `:language`, `:diagnostics`, `:selections`, `:count`, `:pending`, and `:buffer`, in either bar. They provide tab titles, mode, file path/dirty state, caret position, detected language, diagnostics, selection count, numeric prefix, pending key sequence, and buffer identity/count.
|
||||
|
||||
A bar with no visible content consumes no row. For example, `config.bar.top = { left: [], center: [], right: [] }` removes the top bar. A bar whose only visible content is `:tabs` is hidden with a single tab, whether above or below the workspace. Narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped. The command/search prompt and message area remain separate from both bars.
|
||||
|
||||
Each segment's `title` is a literal template supporting only `{index}`, `{name}`, `{path}`, and `{modified}`: the one-based tab index, filename, file path, and dirty marker (`" [+]"` when dirty, `""` when clean). Tabs use each tab's buffer metadata; other segments use the active buffer. Tabs default to `"{index} {name}{modified}"`; an empty title on another segment uses its builtin content or named label value. Templates are not Ruby code and never invoke a render callback.
|
||||
|
||||
`padding` adds spaces inside the segment's styling, on both sides of its title, and accepts integers from `0` through `64` (default `1`). `leading_edge` and `trailing_edge` are separate styled glyph strings, not padding; `""` removes an edge. Tabs default to `"░▒▓"` / `"▓▒░"` edges; mode has only the trailing `"▓▒░"` edge, position only the leading `"░▒▓"` edge, and other segments have none. `first_leading_edge` optionally overrides the first occurrence's leading glyphs: for tabs this is the actual first tab, not the first tab remaining after clipping; other segments have one occurrence. Tabs default this override to `""`, suppressing the first tab's left fade. Unicode widths are measured in terminal cells. Edge strings must be single-line and contain no control characters. Unknown style keys, invalid templates, and wrong types reject configuration.
|
||||
|
||||
Any other segment name is a custom named value, such as `:battery`. Set initial strings with `config.labels`; an empty value hides the custom segment, including its title override, padding, and edges. Names match `[A-Za-z_][A-Za-z0-9_]*`. Labels are plain text: control characters are replaced rather than interpreted as terminal escapes. The same named value can appear on either line.
|
||||
Any other segment name is a custom named value, such as `:battery`. Set initial strings with `config.labels`; an empty value hides the custom segment, including its title override, padding, and edges. Names match `[A-Za-z_][A-Za-z0-9_]*`. Labels are plain text: control characters are replaced rather than interpreted as terminal escapes. The same named value can appear in either bar.
|
||||
|
||||
Configuration runs once at startup. For live values, use [`editor.label(name, text)` or asynchronous plugin updates](plugins.md#named-label-updates). The editor does not collect battery data, run periodic shell commands, or accept render callbacks; a plugin supplies values and chooses when to refresh them.
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ Rust tests live alongside their modules; native LSP integration checks are in [`
|
||||
- Jump-history checks cover cross-buffer/same-document navigation, search and picker recording, counts and repeated visits, bounded history, branch truncation, closed buffers, edited-coordinate clamping, dirty text/undo preservation, Ctrl+i/Tab aliases, and custom bindings. Real LSP fixtures cover definition round-trips and delayed replies after leaving and returning to the same source location.
|
||||
- Tab checks cover shared edits/history with independent pane layouts, focused views, explorers and viewports; cross-tab close guards, immutable snapshots, hidden comparison refresh/result routing, and separate repository workers. Rendered checks cover sanitized/dirty titles, active-tab visibility in narrow terminals, overlay row reservation, and exclusion of hidden diff geometry from palette polling and viewport reporting.
|
||||
- Session and Ruby checks exercise real Ruby execution, command/capability ownership, effect validation, configuration fallback, plugin retirement, crash/timeout recovery, and inherited-pipe cleanup.
|
||||
- For chrome changes, exercise anchor/template validation, padding/edge bounds, named startup values, and atomic live label batches. Rendering regressions should cover full-row center anchoring, terminal-cell glyph widths, empty custom-segment visibility, active-tab preservation, and padding inside styled edges. Labels are session-wide presentation state: test cross-tab/read-only updates and unchanged-value redraw behavior without collecting real battery data or invoking render callbacks.
|
||||
- For chrome changes, exercise anchor/template validation, padding/edge bounds, all builtin/custom segments in either bar, named startup values, and atomic live label batches. Rendering regressions should cover swapped bars across ordinary/split/diff/selector views, full-row center anchoring, terminal-cell glyph widths, empty-content row removal, tabs-only singleton hiding in either position, active-tab preservation, padding inside styled edges, and separate prompt geometry. Labels are session-wide presentation state: test cross-tab/read-only updates and unchanged-value redraw behavior without collecting real battery data or invoking render callbacks.
|
||||
- Unix terminal-hook checks use isolated child processes and real PTYs to keep process-global hooks out of the parallel runner. They cover raw/alternate-screen/mouse-capture cleanup on entry failure, normal return, I/O errors, and panics during frame construction, including original panic-payload propagation.
|
||||
- Redraw checks inspect actual Crossterm output during repeated scrolling: supporting terminals receive synchronized frames, terminals ignoring synchronization never paint with a visible hardware cursor, and the final caret remains visible. A protocol-state regression verifies that the terminal is not left in synchronized-output mode while the next frame is computed.
|
||||
- Wheel checks render document/panel views and cover three-row scrolling, unchanged carets/selections/text, first/last-page clamping, offscreen cursor hiding, no-op/custom-bound navigation reveal, manual persistence through idle/resize/palette updates, and floating/unfocused-docked explorer selection and focus preservation. Queued-input checks verify that the next frame shows ordered wheel steps without crossing a key or resize boundary.
|
||||
|
||||
+4
-2
@@ -218,9 +218,11 @@ Use `:tab.new` to open the current buffer in a new, focused tab with one pane. E
|
||||
| `:tab.close` | `config.core.tab.close` | Close the current tab and all its panes, protecting unsaved changes |
|
||||
| `:tab.force_close` | `config.core.tab.force_close` | Explicitly discard exclusively displayed edits and close the tab |
|
||||
|
||||
Multiple tabs show a single top row with numbered filenames and dirty markers, with padding inside each tab's styling. By default, the active tab has an accent-colored background fading into the terminal background through `░▒▓` / `▓▒░`, except the first tab has no left fade; inactive tabs remain unfilled. Narrow terminals keep the active title visible. New/close actions have no default keybinding; for example, `config.bind "leader t", config.core.tab.new`.
|
||||
By default, multiple tabs show a single top row with numbered filenames and dirty markers, with padding inside each tab's styling. The active tab has an accent-colored background fading into the terminal background through `░▒▓` / `▓▒░`, except the first tab has no left fade; inactive tabs remain unfilled. Narrow terminals keep the active title visible. New/close actions have no default keybinding; for example, `config.bind "leader t", config.core.tab.new`.
|
||||
|
||||
Both the top tabline and bottom statusline use [configurable ordered segments](configuration.md#tabline-and-statusline-segments), anchored left, truly centered on the full row, or anchored right. Title templates, internal padding, and styled edge glyphs can be configured independently. Custom named values such as a battery label are session-wide and can update through plugins without editing buffers. A visible custom segment also shows the top row with a single tab; empty values hide their custom segments. Narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped.
|
||||
The [top and bottom bars](configuration.md#top-and-bottom-bars) accept the same builtin and custom segments, anchored left, truly centered on the full row, or anchored right. Either bar can show tabs or status information, with independent title templates, internal padding, and styled edge glyphs. Defaults keep tabs above and status below; the command/search prompt stays separate. Empty content consumes no row, and a bar whose only visible content is tabs is hidden with a single tab in either position. Custom named values such as a battery label are session-wide and can update through plugins without editing buffers; empty values hide their segments. Both bars share collision handling: narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped.
|
||||
|
||||
At very small heights, both bars yield to the separate active prompt and a document row. If only one bar row fits while both have content, the top bar receives it.
|
||||
|
||||
Closing a tab refuses dirty buffers whose final displayed owner would disappear, including a comparison's working and return buffers. Buffers displayed in another tab remain open with their edits, even on force-close. Other hidden buffers remain loaded. Closing the last tab quits only if every open buffer is clean; force-close quits without saving.
|
||||
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ Navigation carries the source `buffer_id`/`revision`, destination `path`/`row`/`
|
||||
|
||||
## Named label updates
|
||||
|
||||
`editor.label(name, text)` sets a session-wide named value initialized by `config.labels`. Place that name in a [tabline or statusline anchor group](configuration.md#tabline-and-statusline-segments) to display it. This presentation effect does not edit text, move carets, change undo history, or dismiss completion. It also works in read-only diff views.
|
||||
`editor.label(name, text)` sets a session-wide named value initialized by `config.labels`. Place that name in a [top or bottom bar anchor group](configuration.md#top-and-bottom-bars) to display it. This presentation effect does not edit text, move carets, change undo history, or dismiss completion. It also works in read-only diff views.
|
||||
|
||||
```ruby
|
||||
Vamp.plugin "battery_label"
|
||||
@@ -72,7 +72,7 @@ Names match `[A-Za-z_][A-Za-z0-9_]*` and text must be a string. Control characte
|
||||
|
||||
## Messaging
|
||||
|
||||
Rust and Ruby exchange UTF-8, newline-delimited JSON objects with `version: 1`. Startup returns `manifest` (name, required capabilities, commands, async opt-in) or `config` (plugin paths/options, bindings, chrome configuration, and named labels). `invoke` requests carry an ID, command, arguments, and context; `result` replies carry the matching ID and typed effects (`insert`, `replace`, `message`, `label`). A `label` effect carries `name` and `text` strings. Async hosts also receive `document`/`shutdown` and send `update` frames with optional request IDs and typed presentation/navigation effects. Errors use `error` frames. Unknown fields, wrong versions, and mismatched replies are rejected. Frames and aggregate effect text are bounded to 4 MiB.
|
||||
Rust and Ruby exchange UTF-8, newline-delimited JSON objects with `version: 1`. Startup returns `manifest` (name, required capabilities, commands, async opt-in) or `config` (plugin paths/options, bindings, `top_bar` and `bottom_bar` chrome maps, and named labels). `invoke` requests carry an ID, command, arguments, and context; `result` replies carry the matching ID and typed effects (`insert`, `replace`, `message`, `label`). A `label` effect carries `name` and `text` strings. Async hosts also receive `document`/`shutdown` and send `update` frames with optional request IDs and typed presentation/navigation effects. Errors use `error` frames. Unknown fields, wrong versions, and mismatched replies are rejected. Frames and aggregate effect text are bounded to 4 MiB.
|
||||
|
||||
Before loading a script, Rust supplies a bounded startup JSON envelope containing `core_actions` and `options`. The action catalog initializes the Ruby interface; only the separate user-options object is exposed through `Vamp.options`, including when user options contain a key named `core_actions`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user