diff --git a/docs/configuration.md b/docs/configuration.md index faf4533..3877a3c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -117,6 +117,8 @@ config.bind "leader s", config.core.diff.orientation.toggle `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, margins, 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. +The renderer inserts one unstyled blank column between visible items in each group, including tab titles. Hidden items contribute no separator; there is no separator before the first or after the last item. Separator widths count toward anchoring and collision handling. Plain-text segments default to no padding or margins, so adjacent labels do not double their spacing. + Use the two setters independently; a combined `config.bar = { ... }` assignment is not supported. Defaults put `left: [:tabs]` and `right: [:shell, :battery]` in the top bar; the bottom has `left: [:mode, :pending, :buffer, :diagnostics, :path, :selections, :count]` and `right: [:language, :lsp, :position]`; other groups are empty. Set the bars independently, as in this swapped layout with status information above and tabs plus live battery charge below: @@ -127,7 +129,7 @@ Vamp.configure do |config| left: [:mode, :path, :diagnostics, :selections, :count, :pending, :buffer], center: [], right: [:language, :lsp, :position], mode: { padding: 1, leading_edge: "", trailing_edge: "▓░" }, - position: { padding: 1, leading_margin: 1, trailing_margin: 0, leading_edge: "░▓", trailing_edge: "" } + position: { padding: 1, leading_edge: "░▓", trailing_edge: "" } } config.bar.bottom = { left: [:tabs], center: [], right: [:shell, :battery], @@ -150,9 +152,9 @@ Each segment's `title` is a literal template supporting only `{index}`, `{view}` `:lsp` displays only ready servers assigned to the focused buffer, in configured stack order joined with ` + `. Disabled, initializing, failed, or unassigned servers do not appear. Names come from the initialization reply's `serverInfo.name`, falling back to the configured executable basename; no arbitrary named label can make an inactive server appear active. -`margin` adds unstyled space outside both edges of a segment, unlike the styled internal `padding`. `leading_margin` and `trailing_margin` override their respective sides. All accept integers from `0` through `64`. Margins default to zero except the bottom bar's `:position`, which has one leading column, and `:shell`, which has one trailing column separating it from the battery. LSP has no default margins. For example, `config.bar.bottom = { position: { leading_margin: 0 } }` removes position's external spacing without changing its padding. Margins count toward anchor placement and collision handling; empty segments consume no margin, and margins yield along with padding and edges when only clipped content fits. +`margin` adds unstyled space outside both edges of a segment, unlike the styled internal `padding`. `leading_margin` and `trailing_margin` override their respective sides. All accept integers from `0` through `64`; all margins default to zero. Explicit margins are additional to the renderer's one-column separator, so leave them at zero for normal inter-item spacing. Margins count toward anchor placement and collision handling; empty segments consume no margin, and margins yield along with padding and edges when only clipped content fits. -`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 and shell status default to `"░▓"` / `"▓░"` edges; mode has only the trailing `"▓░"` edge, position and battery 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. +`padding` adds spaces inside the segment's styling, on both sides of its title, and accepts integers from `0` through `64`. It defaults to `0` for plain-text and custom segments and `1` for tabs, mode, position, battery, and shell status. Internal padding is separate from the inter-item separator; explicit padding can deliberately add more whitespace. `leading_edge` and `trailing_edge` are separate styled glyph strings, not padding; `""` removes an edge. Tabs and shell status default to `"░▓"` / `"▓░"` edges; mode has only the trailing `"▓░"` edge, position and battery 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 `:build_status`. Set initial strings with `config.labels`; an empty value hides the custom segment, including its title override, margins, 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. @@ -160,7 +162,7 @@ Any other segment name is a custom named value, such as `:build_status`. Set ini Move `:battery` to any anchor group or remove it from both bars to stop battery collection. A nonempty `config.labels[:battery]` or plugin value explicitly overrides the native text and pauses collection; clearing that value restores native readings. Other custom live values use [`editor.label(name, text)` or asynchronous plugin updates](plugins.md#named-label-updates). Configuration runs once at startup; there are no render callbacks. -`:shell` is one shared segment containing the abbreviations and animated spinners for all running `:!COMMAND` processes. Each entry appears only after its command has run for 200 ms, avoiding flashes for short commands. Completed output is shown immediately or queued behind the current result window. The segment hides when idle. It defaults immediately left of `:battery`, with its own styled edges and a separating margin; move it to any anchor group or omit it without affecting command execution. A nonempty label named `shell` overrides the builtin display; clearing it restores the running indicators. +`:shell` is one shared segment containing the abbreviations and animated spinners for all running `:!COMMAND` processes. Each entry appears only after its command has run for 200 ms, avoiding flashes for short commands. Completed output is shown immediately or queued behind the current result window. The segment hides when idle. It defaults immediately left of `:battery`, with its own styled edges and the standard one-column separator; move it to any anchor group or omit it without affecting command execution. A nonempty label named `shell` overrides the builtin display; clearing it restores the running indicators. ### Command and search popup anchors diff --git a/src/chrome.rs b/src/chrome.rs index 94d4bbf..6c3369a 100644 --- a/src/chrome.rs +++ b/src/chrome.rs @@ -102,7 +102,7 @@ pub struct ChromeLine { pub segments: BTreeMap, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SegmentConfig { pub title: String, pub padding: usize, @@ -112,19 +112,6 @@ pub struct SegmentConfig { pub first_leading_edge: Option, } -impl Default for SegmentConfig { - fn default() -> Self { - Self { - title: String::new(), - padding: 1, - margin: [0; 2], - leading_edge: String::new(), - trailing_edge: String::new(), - first_leading_edge: None, - } - } -} - impl ChromeLine { pub fn top() -> Self { Self { @@ -149,13 +136,7 @@ impl ChromeLine { right: vec!["language".into(), "lsp".into(), "position".into()], segments: DEFAULT_BOTTOM_SEGMENTS .iter() - .map(|name| { - let mut segment = default_segment(name); - if *name == "position" { - segment.margin[0] = 1; - } - ((*name).into(), segment) - }) + .map(|name| ((*name).into(), default_segment(name))) .collect(), } } @@ -219,8 +200,8 @@ fn default_segment(name: &str) -> SegmentConfig { if matches!(name, "tabs" | "mode" | "shell") { segment.trailing_edge = "▓░".into(); } - if name == "shell" { - segment.margin[1] = 1; + if matches!(name, "tabs" | "mode" | "position" | "battery" | "shell") { + segment.padding = 1; } if name == "tabs" { segment.title = "{index} {view}{name}{modified}".into(); diff --git a/src/tui.rs b/src/tui.rs index a966a5f..a0b3eb8 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1833,7 +1833,7 @@ fn chrome_items<'a>( } fn chrome_group_width(items: &[ChromeItem<'_>]) -> usize { - items.iter().map(|item| item.width).sum() + items.iter().map(|item| item.width).sum::() + items.len().saturating_sub(1) } fn chrome_fits([left, center, right]: [usize; 3], width: usize) -> bool { @@ -1904,13 +1904,15 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec>; 3], area: Re } break; }; - widths[group] -= groups[group].remove(index).width; + groups[group].remove(index); + widths[group] = chrome_group_width(&groups[group]); } let widths = widths.map(|cells| cells.min(width)); let starts = [0, (width - widths[1]) / 2, width - widths[2]]; for (items, start) in groups.iter().zip(starts) { let mut x = start; - for item in items { + for (index, item) in items.iter().enumerate() { + x += usize::from(index > 0); if x >= width { break; } diff --git a/tests/unit/ruby.rs b/tests/unit/ruby.rs index d167bb4..85c4ce3 100644 --- a/tests/unit/ruby.rs +++ b/tests/unit/ruby.rs @@ -931,7 +931,6 @@ end let loaded = load_config(&path).unwrap(); assert_eq!(loaded.top_bar.left, ["mode", "language"]); assert_eq!(loaded.top_bar.segment("mode").padding, 0); - assert_eq!(loaded.top_bar.segment("language").padding, 1); assert_eq!(loaded.bottom_bar.left, ["tabs"]); assert_eq!(loaded.bottom_bar.segment("tabs").title, "{name}{modified}"); diff --git a/tests/unit/tui.rs b/tests/unit/tui.rs index 20c6306..d10ca23 100644 --- a/tests/unit/tui.rs +++ b/tests/unit/tui.rs @@ -4933,41 +4933,69 @@ fn native_lsp_names_are_borrowed_in_either_bar_and_ignore_configured_labels() { } #[test] -fn bottom_position_has_only_a_leading_margin_and_lsp_has_none() { +fn chrome_separates_visible_segments_once_in_every_anchor() { let mut editor = Editor::open(None, vec![]).unwrap(); - editor.set_lsp_names(vec!["server".into()]); - configure_chrome(&mut editor, |top, bottom| { - top.left.clear(); - top.center.clear(); - top.right = vec!["position".into()]; - bottom.left.clear(); - bottom.center.clear(); - bottom.right = vec!["lsp".into(), "position".into()]; - }); - let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 2)).unwrap(); - terminal - .draw(|frame| { - for (line, y) in [ - (editor.top_bar_config(), 0), - (editor.bottom_bar_config(), 1), - ] { - let groups = chrome_groups(&editor, line, &SyntaxCache::new()); - let start = 40 - chrome_group_width(&groups[2]) as u16; - if y == 1 { - let gap = start + groups[2][0].width as u16; - draw_chrome(frame, groups, Rect::new(0, y, 40, 1)); - assert_eq!(frame.buffer_mut()[(start, y)].bg, Color::Cyan); - assert_eq!(frame.buffer_mut()[(gap, y)].symbol(), " "); - assert_eq!(frame.buffer_mut()[(gap, y)].bg, Color::Reset); - assert_eq!(frame.buffer_mut()[(gap + 1, y)].symbol(), "░"); - } else { - draw_chrome(frame, groups, Rect::new(0, y, 40, 1)); - assert_eq!(frame.buffer_mut()[(start, y)].symbol(), "░"); + for anchor in 0..3 { + configure_chrome(&mut editor, |top, bottom| { + for line in [top, bottom] { + line.left.clear(); + line.center.clear(); + line.right.clear(); + let names = vec![ + "before".into(), + "lsp".into(), + "after".into(), + "position".into(), + ]; + match anchor { + 0 => line.left = names, + 1 => line.center = names, + _ => line.right = names, } - assert_eq!(frame.buffer_mut()[(39, y)].bg, Color::Cyan); } - }) - .unwrap(); + }); + editor + .set_label("before".into(), "Markdown".into()) + .unwrap(); + editor.set_label("after".into(), "tail".into()).unwrap(); + for servers in [vec![], vec!["server".into()]] { + editor.set_lsp_names(servers); + let expected = if editor.lsp_label().is_empty() { + "Markdown tail ░▓ 1:1 " + } else { + "Markdown server tail ░▓ 1:1 " + }; + let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(50, 2)).unwrap(); + terminal + .draw(|frame| { + for (line, y) in [ + (editor.top_bar_config(), 0), + (editor.bottom_bar_config(), 1), + ] { + draw_chrome( + frame, + chrome_groups(&editor, line, &SyntaxCache::new()), + Rect::new(0, y, 50, 1), + ); + } + }) + .unwrap(); + let start = match anchor { + 0 => 0, + 1 => (50 - expected.width()) / 2, + _ => 50 - expected.width(), + }; + for y in 0..2 { + assert_eq!( + pane_screen( + &terminal, + Rect::new(start as u16, y, expected.width() as u16, 1) + ), + expected + ); + } + } + } } #[test] @@ -5855,8 +5883,8 @@ fn chrome_anchors_center_on_the_full_row_and_preserve_right_order() { assert_eq!(buffer[(25, y)].symbol(), "I"); assert_eq!(buffer[(26, y)].symbol(), "D"); } - assert!(pane_screen(&terminal, Rect::new(0, 0, 51, 1)).ends_with("ABC")); - assert!(pane_screen(&terminal, Rect::new(3, 2, 45, 1)).ends_with("ABC")); + assert!(pane_screen(&terminal, Rect::new(0, 0, 51, 1)).ends_with("A BC")); + assert!(pane_screen(&terminal, Rect::new(3, 2, 45, 1)).ends_with("A BC")); editor.set_label("left".into(), "L".repeat(30)).unwrap(); terminal @@ -5876,7 +5904,7 @@ fn chrome_anchors_center_on_the_full_row_and_preserve_right_order() { for (y, x, width) in [(0, 0, 51), (2, 3, 45)] { let row = pane_screen(&terminal, Rect::new(x, y, width, 1)); assert!(!row.contains("MID"), "{row}"); - assert!(row.ends_with("ABC"), "{row}"); + assert!(row.ends_with("A BC"), "{row}"); } } @@ -6024,11 +6052,11 @@ fn chrome_uses_cell_widths_for_independent_edges_and_internal_padding() { let buffer = terminal.backend().buffer(); for (x, text) in [ (2, "1"), - (5, "界"), - (7, "▌"), - (10, "2"), - (13, "▐"), - (14, "界"), + (6, "界"), + (8, "▌"), + (11, "2"), + (14, "▐"), + (15, "界"), (29, "界"), (31, "("), (34, "e\u{301}"), @@ -6051,9 +6079,9 @@ fn chrome_uses_cell_widths_for_independent_edges_and_internal_padding() { assert_eq!(buffer[(x, 2)].bg, Color::Cyan); } assert_eq!(buffer[(2, 0)].bg, Color::Reset); - assert_eq!(buffer[(10, 0)].bg, Color::Cyan); + assert_eq!(buffer[(11, 0)].bg, Color::Cyan); assert_eq!(buffer[(5, 0)].bg, Color::Reset); - for x in [8, 9, 11, 12, 32, 33, 35, 36] { + for x in [9, 10, 12, 13, 32, 33, 35, 36] { assert_eq!(buffer[(x, 0)].symbol(), " "); assert_eq!(buffer[(x, 0)].bg, Color::Cyan); } @@ -6071,8 +6099,8 @@ fn chrome_uses_cell_widths_for_independent_edges_and_internal_padding() { assert_eq!(buffer[(0, 0)].symbol(), "<"); assert_eq!(buffer[(3, 0)].symbol(), "1"); assert_eq!(buffer[(3, 0)].bg, Color::Cyan); - assert_eq!(buffer[(11, 0)].symbol(), "2"); - assert_eq!(buffer[(11, 0)].bg, Color::Reset); + assert_eq!(buffer[(12, 0)].symbol(), "2"); + assert_eq!(buffer[(12, 0)].bg, Color::Reset); } #[test]