feat: add bar element margins and space LSP indicators

This commit is contained in:
Valdemar Lorenzen
2026-10-13 11:20:02 +02:00
parent b94739183d
commit 9f33104cb2
6 changed files with 131 additions and 31 deletions
+20 -25
View File
@@ -106,6 +106,7 @@ pub struct ChromeLine {
pub struct SegmentConfig {
pub title: String,
pub padding: usize,
pub margin: usize,
pub leading_edge: String,
pub trailing_edge: String,
pub first_leading_edge: Option<String>,
@@ -116,6 +117,7 @@ impl Default for SegmentConfig {
Self {
title: String::new(),
padding: 1,
margin: 0,
leading_edge: String::new(),
trailing_edge: String::new(),
first_leading_edge: None,
@@ -167,10 +169,12 @@ impl ChromeLine {
validate_name(name)?;
}
for (name, segment) in &self.segments {
if segment.padding > 64 {
return Err(format!(
"Chrome segment {name:?} padding must be between 0 and 64"
));
for (option, value) in [("padding", segment.padding), ("margin", segment.margin)] {
if value > 64 {
return Err(format!(
"Chrome segment {name:?} {option} must be between 0 and 64"
));
}
}
for (option, text) in [
("title", Some(&segment.title)),
@@ -198,6 +202,9 @@ impl ChromeLine {
fn default_segment(name: &str) -> SegmentConfig {
let mut segment = SegmentConfig::default();
if name == "lsp" {
segment.margin = 1;
}
if matches!(name, "tabs" | "position" | "battery") {
segment.leading_edge = "░▓".into();
}
@@ -258,6 +265,7 @@ struct LineOptions {
struct SegmentOptions {
title: Option<String>,
padding: Option<usize>,
margin: Option<usize>,
leading_edge: Option<String>,
trailing_edge: Option<String>,
first_leading_edge: Option<String>,
@@ -304,6 +312,9 @@ fn deserialize_line<'de, D: Deserializer<'de>>(
if let Some(padding) = options.padding {
segment.padding = padding;
}
if let Some(margin) = options.margin {
segment.margin = margin;
}
if let Some(edge) = options.leading_edge {
segment.leading_edge = edge;
}
@@ -402,27 +413,6 @@ mod tests {
bottom: ChromeLine,
}
#[test]
fn bottom_keeps_metadata_on_the_right_without_filled_styles() {
let line = ChromeLine::bottom();
assert_eq!(
line.left,
[
"mode",
"pending",
"buffer",
"diagnostics",
"path",
"selections",
"count"
]
);
assert_eq!(line.right, ["language", "lsp", "position"]);
for name in ["language", "lsp"] {
assert_eq!(line.segment(name), &SegmentConfig::default());
}
}
#[test]
fn omitted_options_inherit_and_explicit_groups_replace_defaults() {
let lines: Lines = serde_json::from_value(serde_json::json!({
@@ -521,6 +511,11 @@ mod tests {
serde_json::json!({"left": null}),
serde_json::json!({"tabs": {"padding": 65}}),
serde_json::json!({"tabs": {"padding": null}}),
serde_json::json!({"tabs": {"margin": 65}}),
serde_json::json!({"tabs": {"margin": -1}}),
serde_json::json!({"tabs": {"margin": null}}),
serde_json::json!({"tabs": {"margin": "1"}}),
serde_json::json!({"tabs": {"margin": 1.5}}),
serde_json::json!({"tabs": {"leading_edge": "\n"}}),
serde_json::json!({"tabs": {"title": "{unknown}"}}),
serde_json::json!({"tabs": {"title": "{name"}}),
+5
View File
@@ -1363,6 +1363,11 @@ end
("bar.top", r#"{mode: {leading_edge: "\n"}}"#, "single-line"),
("bar.top", "{language: {padding: 65}}", "between 0 and 64"),
("bar.bottom", "{tabs: {padding: '1'}}", "must be an Integer"),
("bar.top", "{lsp: {margin: 65}}", "between 0 and 64"),
("bar.bottom", "{lsp: {margin: -1}}", "between 0 and 64"),
("bar.top", "{lsp: {margin: '1'}}", "must be an Integer"),
("bar.bottom", "{lsp: {margin: 1.5}}", "must be an Integer"),
("bar.top", "{lsp: {margin: nil}}", "must be an Integer"),
("bar.top", "{mode: {title: nil}}", "must be a String"),
(
"bar.bottom",
+2 -2
View File
@@ -35,9 +35,9 @@ module Vamp
chrome_name!(name)
style = chrome_hash!(value).to_h do |option, item|
case option
when "padding"
when "padding", "margin"
unless item.is_a?(Integer) && item.between?(0, 64)
raise ArgumentError, "padding must be an Integer between 0 and 64"
raise ArgumentError, "#{option} must be an Integer between 0 and 64"
end
when "title", "leading_edge", "trailing_edge", "first_leading_edge"
item = string!(item, option)
+98 -1
View File
@@ -1244,6 +1244,7 @@ fn chrome_items<'a>(
for item in &mut items {
item.width = item.text.width()
+ item.config.padding * 2
+ item.config.margin * 2
+ if item.tab.is_none() || item.accent {
item.leading_edge().width() + item.config.trailing_edge.width()
} else {
@@ -1352,15 +1353,17 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec<ChromeItem<'_>>; 3], area: Re
};
let item_width = item.width;
let line = if item.compact || item_width > available {
// Content wins over all padding and fades on tiny terminals.
// Content wins over margins, padding, and fades on tiny terminals.
Line::from(Span::styled(visible_line(&item.text, 0, available), style))
} else {
const PADDING: &str =
" ";
let padding = &PADDING[..item.config.padding];
let margin = &PADDING[..item.config.margin];
let edges = item.tab.is_none() || item.accent;
let edge = Style::default().fg(Color::Cyan);
Line::from(vec![
Span::styled(margin, Style::reset()),
Span::styled(if edges { item.leading_edge() } else { "" }, edge),
Span::styled(padding, style),
Span::styled(item.text.as_ref(), style),
@@ -1373,6 +1376,7 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec<ChromeItem<'_>>; 3], area: Re
},
edge,
),
Span::styled(margin, Style::reset()),
])
};
let cells = item_width.min(available);
@@ -5971,6 +5975,95 @@ mod tests {
}
}
#[test]
fn bar_margins_surround_styling_and_keep_anchors_and_narrow_content() {
let directory = Directory::new();
let config = directory.file(
"margins.rb",
r#"
Vamp.configure do |config|
options = {left: [:badge], center: [:lsp], right: [:badge],
badge: {padding: 0, margin: 2, leading_edge: "<", trailing_edge: ">"}, lsp: {padding: 0}}
config.bar.top = options
config.bar.bottom = options
end
"#,
);
let config = crate::ruby::load_config(&config).unwrap();
let mut editor = Editor::open(None, vec![]).unwrap();
editor
.set_chrome_config(config.top_bar, config.bottom_bar, config.labels)
.unwrap();
editor.set_label("badge".into(), "界".into()).unwrap();
editor.set_lsp_names(vec!["server".into()]);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(41, 3)).unwrap();
terminal
.draw(|frame| {
for (line, y) in [
(editor.top_bar_config(), 0),
(editor.bottom_bar_config(), 2),
] {
draw_chrome(
frame,
chrome_groups(&editor, line, &SyntaxCache::new()),
Rect::new(0, y, 41, 1),
);
}
})
.unwrap();
let buffer = terminal.backend().buffer();
for y in [0, 2] {
for x in [0, 1, 6, 7, 16, 23, 33, 34, 39, 40] {
assert_eq!(buffer[(x, y)].symbol(), " ");
assert_eq!(buffer[(x, y)].bg, Color::Reset);
}
for x in [3, 36] {
assert_eq!(buffer[(x, y)].symbol(), "界");
assert_eq!(buffer[(x, y)].bg, Color::Cyan);
}
for (x, symbol) in [(2, "<"), (5, ">"), (35, "<"), (38, ">")] {
assert_eq!(buffer[(x, y)].symbol(), symbol);
assert_eq!(buffer[(x, y)].fg, Color::Cyan);
}
for (offset, ch) in "server".chars().enumerate() {
assert_eq!(buffer[(17 + offset as u16, y)].symbol(), ch.to_string());
assert_eq!(buffer[(17 + offset as u16, y)].bg, Color::Cyan);
}
}
configure_chrome(&mut editor, |top, bottom| {
for line in [top, bottom] {
line.left = vec!["lsp".into()];
line.center.clear();
line.right.clear();
let lsp = line.segments.get_mut("lsp").unwrap();
lsp.margin = 64;
lsp.padding = 64;
}
});
let mut narrow = Terminal::new(ratatui::backend::TestBackend::new(6, 2)).unwrap();
narrow
.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, 6, 1),
);
}
})
.unwrap();
for y in [0, 1] {
assert_eq!(pane_screen(&narrow, Rect::new(0, y, 6, 1)), "server");
}
editor.set_lsp_names(Vec::new());
for line in [editor.top_bar_config(), editor.bottom_bar_config()] {
assert!(chrome_groups(&editor, line, &SyntaxCache::new())[0].is_empty());
}
}
#[test]
fn floating_fields_use_border_titles_and_inner_horizontal_padding() {
for (prefix, title) in [(':', "Command"), ('/', "Search")] {
@@ -6656,6 +6749,7 @@ mod tests {
SegmentConfig {
title: String::new(),
padding: 0,
margin: 0,
leading_edge: String::new(),
trailing_edge: String::new(),
first_leading_edge: None,
@@ -6819,6 +6913,7 @@ mod tests {
SegmentConfig {
title: "{index}".into(),
padding: 2,
margin: 0,
leading_edge: "界▌".into(),
trailing_edge: "▐界".into(),
first_leading_edge: Some("<".into()),
@@ -6829,6 +6924,7 @@ mod tests {
SegmentConfig {
title: String::new(),
padding: 2,
margin: 0,
leading_edge: "界(".into(),
trailing_edge: ")界".into(),
first_leading_edge: None,
@@ -6925,6 +7021,7 @@ mod tests {
SegmentConfig {
title: "Not visible without a value".into(),
padding: 64,
margin: 0,
leading_edge: "░▒▓".into(),
trailing_edge: "▓▒░".into(),
first_leading_edge: None,