feat: configure interchangeable top and bottom bars

This commit is contained in:
Valdemar Lorenzen
2026-10-12 11:47:31 +02:00
parent 4cfd068f1c
commit 0d3fbc01f8
12 changed files with 776 additions and 340 deletions
+61 -32
View File
@@ -1,7 +1,7 @@
use serde::{Deserialize, Deserializer};
use std::collections::BTreeMap;
const STATUS_SEGMENTS: &[&str] = &[
const DEFAULT_BOTTOM_SEGMENTS: &[&str] = &[
"mode",
"pending",
"buffer",
@@ -43,7 +43,7 @@ impl Default for SegmentConfig {
}
impl ChromeLine {
pub fn tabline() -> Self {
pub fn top() -> Self {
Self {
left: vec!["tabs".into()],
center: Vec::new(),
@@ -52,15 +52,15 @@ impl ChromeLine {
}
}
pub fn statusline() -> Self {
pub fn bottom() -> Self {
Self {
left: STATUS_SEGMENTS[..STATUS_SEGMENTS.len() - 1]
left: DEFAULT_BOTTOM_SEGMENTS[..DEFAULT_BOTTOM_SEGMENTS.len() - 1]
.iter()
.map(|name| (*name).into())
.collect(),
center: Vec::new(),
right: vec!["position".into()],
segments: STATUS_SEGMENTS
segments: DEFAULT_BOTTOM_SEGMENTS
.iter()
.map(|name| ((*name).into(), default_segment(name)))
.collect(),
@@ -72,7 +72,7 @@ impl ChromeLine {
self.segments.get(name).expect("validated chrome segment")
}
pub fn validate(&mut self, tabline: bool) -> Result<(), String> {
pub fn validate(&mut self) -> Result<(), String> {
for name in self
.left
.iter()
@@ -81,13 +81,6 @@ impl ChromeLine {
.chain(self.segments.keys())
{
validate_name(name)?;
if (tabline && STATUS_SEGMENTS.contains(&name.as_str())) || (!tabline && name == "tabs")
{
return Err(format!(
"Chrome segment {name:?} is only available in the {}",
if tabline { "statusline" } else { "tabline" }
));
}
}
for (name, segment) in &self.segments {
if segment.padding > 64 {
@@ -188,7 +181,7 @@ struct SegmentOptions {
fn deserialize_line<'de, D: Deserializer<'de>>(
deserializer: D,
tabline: bool,
mut line: ChromeLine,
) -> Result<ChromeLine, D::Error> {
// Decode through a value so explicit null is rejected rather than treated as an omitted option.
let value = serde_json::Value::deserialize(deserializer)?;
@@ -207,11 +200,6 @@ fn deserialize_line<'de, D: Deserializer<'de>>(
}
}
let options: LineOptions = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
let mut line = if tabline {
ChromeLine::tabline()
} else {
ChromeLine::statusline()
};
if let Some(left) = options.left {
line.left = left;
}
@@ -242,16 +230,16 @@ fn deserialize_line<'de, D: Deserializer<'de>>(
segment.first_leading_edge = Some(edge);
}
}
line.validate(tabline).map_err(serde::de::Error::custom)?;
line.validate().map_err(serde::de::Error::custom)?;
Ok(line)
}
pub fn tabline<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
deserialize_line(deserializer, true)
pub fn top<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
deserialize_line(deserializer, ChromeLine::top())
}
pub fn statusline<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
deserialize_line(deserializer, false)
pub fn bottom<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
deserialize_line(deserializer, ChromeLine::bottom())
}
pub fn labels<'de, D: Deserializer<'de>>(
@@ -270,9 +258,9 @@ mod tests {
#[derive(Deserialize)]
struct Lines {
#[serde(deserialize_with = "tabline")]
#[serde(deserialize_with = "top")]
top: ChromeLine,
#[serde(deserialize_with = "statusline")]
#[serde(deserialize_with = "bottom")]
bottom: ChromeLine,
}
@@ -290,11 +278,51 @@ mod tests {
assert_eq!(lines.bottom.segment("workspace"), &SegmentConfig::default());
}
#[test]
fn both_positions_accept_all_builtins_and_custom_styles() {
let names = [
"tabs",
"mode",
"pending",
"buffer",
"diagnostics",
"path",
"selections",
"count",
"language",
"position",
"workspace",
];
let options = serde_json::json!({
"left": names,
"center": ["language"],
"right": ["tabs"],
"tabs": {"title": "{index}:{name}:{path}{modified}", "padding": 0},
"mode": {"leading_edge": "<", "trailing_edge": ">"},
"workspace": {"first_leading_edge": ""}
});
let lines: Lines = serde_json::from_value(serde_json::json!({
"top": options,
"bottom": options,
}))
.unwrap();
for line in [&lines.top, &lines.bottom] {
assert_eq!(line.left, names);
assert_eq!(line.center, ["language"]);
assert_eq!(line.right, ["tabs"]);
assert_eq!(line.segment("tabs").padding, 0);
assert_eq!(line.segment("mode").leading_edge, "<");
assert_eq!(
line.segment("workspace").first_leading_edge.as_deref(),
Some("")
);
}
}
#[test]
fn malformed_chrome_is_rejected_at_the_boundary() {
for top in [
for options in [
serde_json::json!({"left": ["bad-name"]}),
serde_json::json!({"left": ["mode"]}),
serde_json::json!({"left": null}),
serde_json::json!({"tabs": {"padding": 65}}),
serde_json::json!({"tabs": {"padding": null}}),
@@ -304,10 +332,11 @@ mod tests {
serde_json::json!({"tabs": {"title": "name}"}}),
serde_json::json!({"tabs": {"colour": "red"}}),
] {
assert!(
serde_json::from_value::<Lines>(serde_json::json!({"top": top, "bottom": {}}))
.is_err()
);
for position in ["top", "bottom"] {
let mut frame = serde_json::json!({"top": {}, "bottom": {}});
frame[position] = options.clone();
assert!(serde_json::from_value::<Lines>(frame).is_err());
}
}
}
}