use super::*;
use crate::input::{ModelInfo, StatusContext, Tool, WorkspaceInfo};
use crate::theme;
use std::borrow::Cow;
use std::path::PathBuf;
use std::sync::Arc;
struct NoopSegment;
impl Segment for NoopSegment {
fn render(&self, _ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
Ok(None)
}
}
static NOOP: NoopSegment = NoopSegment;
fn noop_segment() -> &'static dyn Segment {
&NOOP
}
static TEST_SEG_ID: Cow<'static, str> = Cow::Borrowed("test");
fn empty_ctx() -> DataContext {
DataContext::new(StatusContext {
tool: Tool::ClaudeCode,
model: Some(ModelInfo {
display_name: "X".into(),
}),
workspace: Some(WorkspaceInfo {
project_dir: PathBuf::from("/"),
git_worktree: None,
}),
context_window: None,
cost: None,
effort: None,
vim: None,
output_style: None,
agent_name: None,
version: None,
raw: Arc::new(serde_json::Value::Null),
})
}
fn empty_rc() -> RenderContext {
RenderContext::new(80)
}
fn item(text: &str, priority: u8) -> LayoutItem<'static> {
LayoutItem::Segment(SegmentEntry {
id: &TEST_SEG_ID,
rendered: RenderedSegment::new(text),
defaults: SegmentDefaults::with_priority(priority),
segment: noop_segment(),
})
}
fn space() -> LayoutItem<'static> {
LayoutItem::Separator(Separator::Space)
}
fn pl() -> LayoutItem<'static> {
LayoutItem::Separator(Separator::powerline())
}
fn lit(s: &'static str) -> LayoutItem<'static> {
LayoutItem::Separator(Separator::Literal(Cow::Borrowed(s)))
}
fn none_sep() -> LayoutItem<'static> {
LayoutItem::Separator(Separator::None)
}
fn spaced(specs: &[(&str, u8)]) -> Vec<LayoutItem<'static>> {
interleaved(specs, space)
}
fn pl_spec(specs: &[(&str, u8)]) -> Vec<LayoutItem<'static>> {
interleaved(specs, pl)
}
fn interleaved(
specs: &[(&str, u8)],
mut sep: impl FnMut() -> LayoutItem<'static>,
) -> Vec<LayoutItem<'static>> {
let mut out = Vec::with_capacity(specs.len().saturating_mul(2));
for (i, &(text, priority)) in specs.iter().enumerate() {
out.push(item(text, priority));
if i + 1 < specs.len() {
out.push(sep());
}
}
out
}
macro_rules! let_noop_observers {
($name:ident) => {
let mut __warn_for_observers: fn(&str) = |_: &str| {};
let mut $name = LayoutObservers::new(&mut __warn_for_observers);
};
}
fn line_items_with(segments: Vec<Box<dyn Segment>>, sep: Separator) -> Vec<LineItem> {
let n = segments.len();
let mut out = Vec::with_capacity(n.saturating_mul(2));
for (i, segment) in segments.into_iter().enumerate() {
out.push(LineItem::Segment {
id: std::borrow::Cow::Owned(format!("seg{i}")),
segment,
});
if i + 1 < n {
out.push(LineItem::Separator(sep.clone()));
}
}
out
}
fn line_items_spaced(segments: Vec<Box<dyn Segment>>) -> Vec<LineItem> {
line_items_with(segments, Separator::Space)
}
fn render_plain(items: Vec<LayoutItem<'_>>, terminal_width: u16) -> String {
render_items(
items,
&empty_ctx(),
&empty_rc(),
terminal_width,
theme::default_theme(),
theme::Capability::None,
)
}
#[test]
fn render_items_wraps_each_styled_segment_under_palette16() {
use crate::theme::Role;
let items = vec![
item("a", 10),
space(),
LayoutItem::Segment(SegmentEntry {
id: &TEST_SEG_ID,
rendered: RenderedSegment::new("b").with_role(Role::Warning),
defaults: SegmentDefaults::with_priority(10),
segment: noop_segment(),
}),
space(),
item("c", 10),
];
let out = render_items(
items,
&empty_ctx(),
&empty_rc(),
100,
theme::default_theme(),
theme::Capability::Palette16,
);
assert_eq!(out, "a \x1b[93mb\x1b[0m c");
}
#[test]
fn total_width_sums_all_layout_items() {
let items = spaced(&[("ab", 10), ("cd", 10), ("ef", 10)]);
assert_eq!(total_width(&items), 8);
}
#[test]
fn total_width_zero_for_empty() {
assert_eq!(total_width(&[]), 0);
}
#[test]
fn total_width_single_segment_has_no_separator() {
let items = vec![item("abcde", 10)];
assert_eq!(total_width(&items), 5);
}
#[test]
fn no_width_pressure_renders_all_with_separators() {
let items = spaced(&[("one", 10), ("two", 20), ("three", 30)]);
assert_eq!(render_plain(items, 100), "one two three");
}
#[test]
fn drops_highest_priority_under_pressure() {
let items = spaced(&[
("aaaa", 10),
("bbbb", 200), ("cccc", 50),
]);
let out = render_plain(items, 10);
assert!(!out.contains("bbbb"));
assert!(out.contains("aaaa"));
assert!(out.contains("cccc"));
}
#[test]
fn drops_in_descending_priority_order() {
let items = spaced(&[
("one", 10),
("two", 200), ("three", 20),
("four", 150), ("five", 30),
]);
assert_eq!(render_plain(items, 15), "one three five");
}
#[test]
fn priority_zero_never_drops_even_over_budget() {
let items = spaced(&[("aaaa", 0), ("bbbb", 0)]);
let out = render_plain(items, 3);
assert_eq!(out, "aaaa bbbb");
}
#[test]
fn priority_drop_recomputes_budget_with_powerline_separators() {
let items = pl_spec(&[("aaaa", 0), ("bbbb", 200), ("cccc", 0)]);
let out = render_plain(items, 14);
assert!(out.contains("aaaa"));
assert!(!out.contains("bbbb"));
assert!(out.contains("cccc"));
assert!(
out.contains('\u{E0B0}'),
"chevron survives the drop: {out:?}"
);
}
#[test]
fn mix_drops_positives_keeps_zeros() {
let items = spaced(&[("keep-me", 0), ("droppable", 200), ("sticky", 0)]);
let out = render_plain(items, 20);
assert_eq!(out, "keep-me sticky");
}
#[test]
fn no_trailing_separator() {
let items = spaced(&[("a", 10), ("b", 10)]);
assert_eq!(render_plain(items, 100), "a b");
}
#[test]
fn empty_input_renders_empty_string() {
assert_eq!(render_plain(vec![], 100), "");
}
#[test]
fn respects_inline_literal_separator() {
let items = vec![item("a", 10), lit(" | "), item("b", 10)];
assert_eq!(render_plain(items, 100), "a | b");
}
#[test]
fn render_inline_none_separator_collapses_neighbors() {
let items = vec![item("a", 10), none_sep(), item("b", 10)];
assert_eq!(render_plain(items, 100), "ab");
}
#[test]
fn apply_width_bounds_drops_below_min() {
let bounds = WidthBounds::new(5, 10);
let rendered = RenderedSegment::new("abc"); let_noop_observers!(observers);
assert!(apply_width_bounds(rendered, bounds, &TEST_SEG_ID, &mut observers).is_none());
}
#[test]
fn apply_width_bounds_truncates_above_max() {
let bounds = WidthBounds::new(0, 5);
let rendered = RenderedSegment::new("abcdefghij"); let_noop_observers!(observers);
let truncated =
apply_width_bounds(rendered, bounds, &TEST_SEG_ID, &mut observers).expect("truncated");
assert_eq!(truncated.width, 5);
assert!(truncated.text.ends_with('…'));
assert_eq!(truncated.text, "abcd…");
}
#[test]
fn apply_width_bounds_passthrough_within_range() {
let bounds = WidthBounds::new(2, 10);
let original = RenderedSegment::new("hello");
let_noop_observers!(observers);
let result =
apply_width_bounds(original.clone(), bounds, &TEST_SEG_ID, &mut observers).expect("kept");
assert_eq!(result, original);
}
#[test]
fn apply_width_bounds_none_is_passthrough() {
let original = RenderedSegment::new("anything");
let_noop_observers!(observers);
let result =
apply_width_bounds(original.clone(), None, &TEST_SEG_ID, &mut observers).expect("kept");
assert_eq!(result, original);
}
#[test]
fn truncate_to_zero_yields_empty() {
let out = truncate_to(RenderedSegment::new("abc"), 0);
assert_eq!(out.text, "");
assert_eq!(out.width, 0);
}
#[test]
fn truncate_handles_wide_grapheme_without_splitting() {
let bounds = WidthBounds::new(0, 6);
let_noop_observers!(observers);
let truncated = apply_width_bounds(
RenderedSegment::new("42% · 200k"),
bounds,
&TEST_SEG_ID,
&mut observers,
)
.expect("truncated");
assert_eq!(truncated.text, "42% ·…");
assert_eq!(truncated.width, 6);
}
#[test]
fn truncate_preserves_combining_mark_with_base() {
let r = RenderedSegment::new("ab\u{65}\u{301}de");
assert_eq!(r.width, 5);
let out = truncate_to(r, 4);
assert_eq!(out.text, "ab\u{65}\u{301}…");
assert_eq!(out.width, 4);
}
#[test]
fn truncate_does_not_split_zwj_emoji_sequence() {
let text = "a\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F466}b";
let r = RenderedSegment::new(text);
let out = truncate_to(r, 3);
assert_eq!(out.text, "a…");
assert_eq!(out.width, 2);
}
#[test]
fn truncate_to_max_cells_one_emits_only_ellipsis() {
let r = RenderedSegment::new("anything");
let out = truncate_to(r, 1);
assert_eq!(out.text, "…");
assert_eq!(out.width, 1);
}
#[test]
fn priority_ties_drop_rightmost_first() {
let items = spaced(&[("left", 200), ("mid", 50), ("right", 200)]);
assert_eq!(render_plain(items, 10), "left mid");
}
#[test]
fn separator_none_not_charged_to_budget() {
let items = vec![
item("a", 200),
space(),
item("b", 200),
none_sep(),
item("c", 200),
];
assert_eq!(render_plain(items, 4), "a bc");
}
#[test]
fn total_width_returns_u32_beyond_u16_range() {
fn wide(text: String) -> LayoutItem<'static> {
LayoutItem::Segment(SegmentEntry {
id: &TEST_SEG_ID,
rendered: RenderedSegment::new(text),
defaults: SegmentDefaults::with_priority(10),
segment: noop_segment(),
})
}
let items = vec![
wide("x".repeat(u16::MAX as usize)),
space(),
wide("x".repeat(u16::MAX as usize)),
space(),
wide("x".repeat(u16::MAX as usize)),
];
assert_eq!(total_width(&items), 3 * u32::from(u16::MAX) + 2);
}
#[test]
fn all_priority_zero_keeps_every_segment_even_when_overfull() {
let items = spaced(&[("aaa", 0), ("bbb", 0), ("ccc", 0)]);
assert_eq!(render_plain(items, 4), "aaa bbb ccc");
}
use crate::segments::{RenderResult, SegmentError};
struct StubSegment(RenderResult);
impl Segment for StubSegment {
fn render(&self, _ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
match &self.0 {
Ok(Some(r)) => Ok(Some(r.clone())),
Ok(None) => Ok(None),
Err(e) => Err(SegmentError::new(e.message.clone())),
}
}
}
#[test]
fn segment_error_is_logged_and_hides_segment() {
let line = line_items_spaced(vec![
Box::new(StubSegment(Ok(Some(RenderedSegment::new("ok-before"))))),
Box::new(StubSegment(Err(SegmentError::new("boom")))),
Box::new(StubSegment(Ok(Some(RenderedSegment::new("ok-after"))))),
]);
let mut warnings = Vec::new();
let mut warn = |msg: &str| warnings.push(msg.to_string());
let mut observers = LayoutObservers::new(&mut warn);
let items = collect_items_with(&line, &empty_ctx(), &empty_rc(), &mut observers);
assert_eq!(items.len(), 3);
assert_eq!(segment_text(&items[0]), "ok-before");
assert!(matches!(items[1], LayoutItem::Separator(_)));
assert_eq!(segment_text(&items[2]), "ok-after");
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("segment error"));
assert!(warnings[0].contains("boom"));
}
#[test]
fn ok_none_is_silently_hidden() {
let line = line_items_spaced(vec![
Box::new(StubSegment(Ok(Some(RenderedSegment::new("visible"))))),
Box::new(StubSegment(Ok(None))),
]);
let mut warnings = Vec::new();
let mut warn = |msg: &str| warnings.push(msg.to_string());
let mut observers = LayoutObservers::new(&mut warn);
let items = collect_items_with(&line, &empty_ctx(), &empty_rc(), &mut observers);
assert_eq!(items.len(), 1);
assert_eq!(segment_text(&items[0]), "visible");
assert!(warnings.is_empty());
}
struct WidthEcho;
impl Segment for WidthEcho {
fn render(&self, _ctx: &DataContext, rc: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new(rc.terminal_width.to_string())))
}
}
#[test]
fn render_context_threads_terminal_width_into_segments() {
let line = line_items_spaced(vec![Box::new(WidthEcho)]);
let mut warnings = Vec::new();
let mut warn = |msg: &str| warnings.push(msg.to_string());
let mut observers = LayoutObservers::new(&mut warn);
let rc = RenderContext::new(42);
let items = collect_items_with(&line, &empty_ctx(), &rc, &mut observers);
assert_eq!(items.len(), 1);
assert_eq!(segment_text(&items[0]), "42");
}
#[test]
fn render_with_observers_constructs_render_context_from_terminal_width_arg() {
let line = line_items_spaced(vec![Box::new(WidthEcho)]);
let mut warnings = Vec::new();
let mut warn = |msg: &str| warnings.push(msg.to_string());
let mut observers = LayoutObservers::new(&mut warn);
let out = render_with_observers(
&line,
&empty_ctx(),
137,
&mut observers,
theme::default_theme(),
theme::Capability::None,
false,
);
assert!(out.contains("137"), "got {out:?}");
}
fn truncatable_item(text: &str, priority: u8) -> LayoutItem<'static> {
LayoutItem::Segment(SegmentEntry {
id: &TEST_SEG_ID,
rendered: RenderedSegment::new(text),
defaults: SegmentDefaults::with_priority(priority).with_truncatable(true),
segment: noop_segment(),
})
}
#[track_caller]
fn segment_text<'a>(item: &'a LayoutItem<'_>) -> &'a str {
match item {
LayoutItem::Segment(seg) => &seg.rendered.text,
LayoutItem::Separator(_) => panic!("expected segment, got separator"),
}
}
#[test]
fn reflow_truncates_highest_priority_before_dropping() {
let items = vec![
truncatable_item("linesmith/very-long-feature-branch-name", 200),
space(),
item("Sonnet", 0),
];
let out = render_plain(items, 30);
assert!(out.starts_with("linesmith/very-long-fe"), "got {out:?}");
assert!(out.ends_with("… Sonnet"), "got {out:?}");
assert_eq!(text_width(&out), 30);
}
#[test]
fn reflow_drops_when_truncation_would_fall_below_floor() {
let items = vec![
truncatable_item("workspace-name", 200),
space(),
item("KEEP", 0),
];
let out = render_plain(items, 4);
assert_eq!(out, "KEEP");
}
fn truncatable_with_bounds(text: &str, priority: u8, bounds: WidthBounds) -> LayoutItem<'static> {
LayoutItem::Segment(SegmentEntry {
id: &TEST_SEG_ID,
rendered: RenderedSegment::new(text),
defaults: SegmentDefaults::with_priority(priority)
.with_truncatable(true)
.with_width(bounds),
segment: noop_segment(),
})
}
#[test]
fn reflow_respects_explicit_width_min_floor() {
let bounds = WidthBounds::new(8, u16::MAX).expect("valid");
let items = vec![
truncatable_with_bounds("abcdefghijklmnop", 200, bounds), space(),
item("X", 0),
];
let out = render_plain(items, 10);
assert!(out.contains('…'), "got {out:?}");
assert!(out.ends_with(" X"), "got {out:?}");
let items = vec![
truncatable_with_bounds("abcdefghijklmnop", 200, bounds),
space(),
item("X", 0),
];
let out = render_plain(items, 9);
assert_eq!(out, "X");
}
#[test]
fn non_truncatable_drops_unchanged_under_pressure() {
let items = spaced(&[("45% · 200k", 200), ("Sonnet", 0)]);
let out = render_plain(items, 10);
assert_eq!(out, "Sonnet");
}
#[test]
fn reflow_iterates_when_first_truncation_insufficient() {
let items = vec![
truncatable_item("aaaaaaaaaa", 100),
space(),
truncatable_item("bbbbbbbbbb", 100),
space(),
item("KEEP", 0),
];
let out = render_plain(items, 12);
assert_eq!(out, "aaaaaa… KEEP");
assert_eq!(text_width(&out), 12);
}
#[test]
fn reflow_does_not_touch_priority_zero_even_when_truncatable() {
let items = vec![
LayoutItem::Segment(SegmentEntry {
id: &TEST_SEG_ID,
rendered: RenderedSegment::new("untouchable-long-name"),
defaults: SegmentDefaults::with_priority(0).with_truncatable(true),
segment: noop_segment(),
}),
space(),
item("Sonnet", 0),
];
let out = render_plain(items, 5);
assert_eq!(out, "untouchable-long-name Sonnet");
}
struct ShrinkableSegment {
full: &'static str,
compact: &'static str,
}
impl Segment for ShrinkableSegment {
fn render(&self, _ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new(self.full)))
}
fn shrink_to_fit(
&self,
_ctx: &DataContext,
_rc: &RenderContext,
target: u16,
) -> Option<RenderedSegment> {
let r = RenderedSegment::new(self.compact);
(r.width <= target).then_some(r)
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(200)
}
}
struct AnchorSegment(&'static str);
impl Segment for AnchorSegment {
fn render(&self, _ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new(self.0)))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(0)
}
}
#[test]
fn shrink_to_fit_replaces_full_render_when_compact_form_fits() {
let items = line_items_spaced(vec![
Box::new(ShrinkableSegment {
full: "longbranch * ↑2 ↓1",
compact: "longbranch",
}),
Box::new(AnchorSegment("KEEP")),
]);
let mut warnings = Vec::new();
let mut warn = |m: &str| warnings.push(m.to_string());
let mut observers = LayoutObservers::new(&mut warn);
let line = render_with_observers(
&items,
&empty_ctx(),
17,
&mut observers,
theme::default_theme(),
theme::Capability::None,
false,
);
assert_eq!(line, "longbranch KEEP");
}
#[test]
fn shrink_to_fit_falls_back_to_drop_when_compact_form_too_wide() {
let items = line_items_spaced(vec![
Box::new(ShrinkableSegment {
full: "longbranch",
compact: "stilltoolongtruly",
}),
Box::new(AnchorSegment("X")),
]);
let mut warnings = Vec::new();
let mut warn = |m: &str| warnings.push(m.to_string());
let mut observers = LayoutObservers::new(&mut warn);
let line = render_with_observers(
&items,
&empty_ctx(),
5,
&mut observers,
theme::default_theme(),
theme::Capability::None,
false,
);
assert_eq!(line, "X");
}
#[test]
fn shrink_to_fit_honors_configured_width_min_floor() {
struct LowFloorShrink;
impl Segment for LowFloorShrink {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("longerprefix")))
}
fn shrink_to_fit(
&self,
_: &DataContext,
_: &RenderContext,
_target: u16,
) -> Option<RenderedSegment> {
Some(RenderedSegment::new("five5"))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(200)
.with_width(WidthBounds::new(8, u16::MAX).expect("valid"))
}
}
let items = line_items_spaced(vec![Box::new(LowFloorShrink), Box::new(AnchorSegment("X"))]);
let_noop_observers!(observers);
let line = render_with_observers(
&items,
&empty_ctx(),
7,
&mut observers,
theme::default_theme(),
theme::Capability::None,
false,
);
assert_eq!(line, "X");
}
#[test]
fn shrink_to_fit_rejects_too_wide_response_and_drops() {
struct MisbehavingSegment;
impl Segment for MisbehavingSegment {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("longbranch")))
}
fn shrink_to_fit(
&self,
_: &DataContext,
_: &RenderContext,
_target: u16,
) -> Option<RenderedSegment> {
Some(RenderedSegment::new("stilltoolongtruly"))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(200)
}
}
let items = line_items_spaced(vec![
Box::new(MisbehavingSegment),
Box::new(AnchorSegment("X")),
]);
let_noop_observers!(observers);
let line = render_with_observers(
&items,
&empty_ctx(),
5,
&mut observers,
theme::default_theme(),
theme::Capability::None,
false,
);
assert_eq!(line, "X");
}
#[test]
fn shrink_to_fit_runs_before_truncatable_end_ellipsis() {
struct DualSegment;
impl Segment for DualSegment {
fn render(&self, _ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("longprefix-with-tail")))
}
fn shrink_to_fit(
&self,
_ctx: &DataContext,
_rc: &RenderContext,
target: u16,
) -> Option<RenderedSegment> {
let r = RenderedSegment::new("longprefix");
(r.width <= target).then_some(r)
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(200).with_truncatable(true)
}
}
let items = line_items_spaced(vec![
Box::new(DualSegment),
Box::new(StubSegment(Ok(Some(RenderedSegment::new("X"))))),
]);
let mut warnings = Vec::new();
let mut warn = |m: &str| warnings.push(m.to_string());
let mut observers = LayoutObservers::new(&mut warn);
let line = render_with_observers(
&items,
&empty_ctx(),
13,
&mut observers,
theme::default_theme(),
theme::Capability::None,
false,
);
assert!(line.contains("longprefix"), "got {line:?}");
assert!(!line.contains('…'), "no end-ellipsis: {line:?}");
}
#[test]
fn render_to_runs_empty_input_yields_no_runs() {
let items: Vec<LineItem> = vec![];
let_noop_observers!(observers);
let runs = render_to_runs(&items, &empty_ctx(), 100, &mut observers);
assert!(runs.is_empty());
}
#[test]
fn render_to_runs_emits_segment_then_separator_then_segment() {
let items = line_items_spaced(vec![
Box::new(StubSegment(Ok(Some(RenderedSegment::new("a"))))),
Box::new(StubSegment(Ok(Some(RenderedSegment::new("b"))))),
]);
let_noop_observers!(observers);
let runs = render_to_runs(&items, &empty_ctx(), 100, &mut observers);
assert_eq!(runs.len(), 3);
assert_eq!(runs[0].text, "a");
assert_eq!(runs[0].style, Style::default());
assert_eq!(runs[1].text, " ");
assert_eq!(runs[1].style, Style::default());
assert_eq!(runs[2].text, "b");
assert_eq!(runs[2].style, Style::default());
}
#[test]
fn render_to_runs_preserves_segment_style() {
use crate::theme::Role;
let items = line_items_spaced(vec![
Box::new(StubSegment(Ok(Some(RenderedSegment::new("plain"))))),
Box::new(StubSegment(Ok(Some(
RenderedSegment::new("warn").with_role(Role::Warning),
)))),
]);
let_noop_observers!(observers);
let runs = render_to_runs(&items, &empty_ctx(), 100, &mut observers);
assert_eq!(runs.len(), 3);
assert_eq!(runs[2].text, "warn");
assert_eq!(runs[2].style.role, Some(Role::Warning));
}
#[test]
fn render_to_runs_skips_separator_none_between_segments() {
let items = line_items_with(
vec![
Box::new(StubSegment(Ok(Some(RenderedSegment::with_separator(
"a",
Separator::None,
))))),
Box::new(StubSegment(Ok(Some(RenderedSegment::new("b"))))),
],
Separator::Space,
);
let_noop_observers!(observers);
let runs = render_to_runs(&items, &empty_ctx(), 100, &mut observers);
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].text, "a");
assert_eq!(runs[1].text, "b");
}
#[test]
fn render_to_runs_drops_segments_under_width_pressure() {
let items = line_items_spaced(vec![
Box::new(StubSegment(Ok(Some(
RenderedSegment::new("keep").with_role(crate::theme::Role::Primary),
)))),
Box::new(DroppableStub("droppable")),
Box::new(StubSegment(Ok(Some(RenderedSegment::new("anchor"))))),
]);
let_noop_observers!(observers);
let runs = render_to_runs(&items, &empty_ctx(), 12, &mut observers);
let texts: Vec<&str> = runs.iter().map(|r| r.text.as_str()).collect();
assert_eq!(texts, vec!["keep", " ", "anchor"]);
}
fn round_trip_line() -> Vec<LineItem> {
use crate::theme::Role;
line_items_spaced(vec![
Box::new(StubSegment(Ok(Some(
RenderedSegment::new("ctx").with_role(Role::Info),
)))),
Box::new(StubSegment(Ok(Some(RenderedSegment::new("|"))))),
Box::new(StubSegment(Ok(Some(
RenderedSegment::new("err").with_role(Role::Error),
)))),
])
}
fn round_trip_assert(terminal_width: u16, capability: theme::Capability, hyperlinks: bool) {
let items = round_trip_line();
let_noop_observers!(observers_direct);
let direct = render_with_observers(
&items,
&empty_ctx(),
terminal_width,
&mut observers_direct,
theme::default_theme(),
capability,
hyperlinks,
);
let_noop_observers!(observers_runs);
let runs = render_to_runs(&items, &empty_ctx(), terminal_width, &mut observers_runs);
let recomposed = runs_to_ansi(&runs, theme::default_theme(), capability, hyperlinks);
assert_eq!(
direct, recomposed,
"cap={capability:?} width={terminal_width} hyperlinks={hyperlinks}"
);
}
#[test]
fn render_to_runs_then_runs_to_ansi_matches_render_with_observers() {
round_trip_assert(100, theme::Capability::Palette16, false);
}
#[test]
fn render_to_runs_round_trip_holds_under_capability_none() {
round_trip_assert(100, theme::Capability::None, false);
}
#[test]
fn render_to_runs_round_trip_holds_under_width_pressure() {
round_trip_assert(5, theme::Capability::Palette16, false);
}
#[test]
fn render_to_runs_round_trip_holds_with_hyperlinks_enabled() {
round_trip_assert(100, theme::Capability::Palette16, true);
}
#[test]
fn render_to_runs_with_one_survivor_emits_no_trailing_separator() {
let items = line_items_spaced(vec![
Box::new(StubSegment(Ok(Some(RenderedSegment::new("a"))))),
Box::new(DroppableStub("droppable")),
]);
let_noop_observers!(observers);
let runs = render_to_runs(&items, &empty_ctx(), 1, &mut observers);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].text, "a");
}
#[test]
fn render_to_runs_emits_powerline_chevron_with_muted_role() {
use crate::theme::Role;
let items = line_items_with(
vec![
Box::new(StubSegment(Ok(Some(
RenderedSegment::new("a").with_role(Role::Primary),
)))),
Box::new(StubSegment(Ok(Some(RenderedSegment::new("b"))))),
],
Separator::powerline(),
);
let_noop_observers!(observers);
let runs = render_to_runs(&items, &empty_ctx(), 100, &mut observers);
assert_eq!(runs.len(), 3);
assert_eq!(runs[1].text, " \u{E0B0} ");
assert_eq!(runs[1].style.role, Some(Role::Muted));
}
#[test]
fn powerline_separator_emits_padded_chevron_with_correct_width() {
assert_eq!(Separator::powerline().width(), 3);
assert_eq!(Separator::powerline().text(), " \u{E0B0} ");
}
#[test]
fn powerline_chevrons_are_charged_to_total_width_in_layout() {
let items = pl_spec(&[("aaaa", 0), ("bbbb", 0), ("cccc", 0)]);
let chev = u32::from(Separator::powerline().width());
assert_eq!(total_width(&items), 4 + chev + 4 + chev + 4);
}
#[test]
fn render_with_observers_emits_powerline_chevron_wrapped_in_muted_sgr() {
struct RoledSeg(&'static str, theme::Role);
impl Segment for RoledSeg {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new(self.0).with_role(self.1)))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(10)
}
}
let items = line_items_with(
vec![
Box::new(RoledSeg("a", theme::Role::Primary)),
Box::new(RoledSeg("b", theme::Role::Info)),
],
Separator::powerline(),
);
let_noop_observers!(observers);
let line = render_with_observers(
&items,
&empty_ctx(),
100,
&mut observers,
theme::default_theme(),
theme::Capability::Palette16,
false,
);
let muted_sgr = theme::sgr_open(
&Style::role(theme::Role::Muted),
theme::default_theme(),
theme::Capability::Palette16,
);
let expected = format!("{muted_sgr} \u{E0B0} \x1b[0m");
assert!(
line.contains(&expected),
"padded chevron with Muted SGR not in line: {line:?} (expected substring: {expected:?})"
);
}
#[test]
fn render_to_runs_emits_literal_separator_with_default_style() {
let items = line_items_with(
vec![
Box::new(StubSegment(Ok(Some(
RenderedSegment::new("a").with_role(crate::theme::Role::Warning),
)))),
Box::new(StubSegment(Ok(Some(RenderedSegment::new("b"))))),
],
Separator::Literal(Cow::Borrowed(" | ")),
);
let_noop_observers!(observers);
let runs = render_to_runs(&items, &empty_ctx(), 100, &mut observers);
assert_eq!(runs.len(), 3);
assert_eq!(runs[1].text, " | ");
assert_eq!(runs[1].style, Style::default());
}
#[test]
fn runs_to_ansi_emits_osc8_around_styled_run_when_hyperlinks_supported() {
use crate::theme::Role;
let runs = vec![StyledRun::new(
"branch",
Style::role(Role::Primary).with_hyperlink("https://example.com/b"),
)];
let out = runs_to_ansi(
&runs,
theme::default_theme(),
theme::Capability::Palette16,
true,
);
assert_eq!(
out, "\x1b]8;;https://example.com/b\x1b\\\x1b[95mbranch\x1b[0m\x1b]8;;\x1b\\",
"got {out:?}"
);
}
#[test]
fn runs_to_ansi_drops_hyperlink_when_not_supported() {
use crate::theme::Role;
let runs = vec![StyledRun::new(
"branch",
Style::role(Role::Primary).with_hyperlink("https://example.com/b"),
)];
let out = runs_to_ansi(
&runs,
theme::default_theme(),
theme::Capability::Palette16,
false,
);
assert_eq!(out, "\x1b[95mbranch\x1b[0m");
assert!(!out.contains("\x1b]8"), "no OSC 8: {out:?}");
}
#[test]
fn runs_to_ansi_emits_no_osc8_when_style_has_no_hyperlink() {
let runs = vec![StyledRun::new("plain", Style::default())];
let out = runs_to_ansi(&runs, theme::default_theme(), theme::Capability::None, true);
assert_eq!(out, "plain");
assert!(!out.contains("\x1b]8"), "no OSC 8: {out:?}");
}
#[test]
fn runs_to_ansi_emits_osc8_around_unstyled_run() {
let runs = vec![StyledRun::new(
"click",
Style::default().with_hyperlink("https://example.com"),
)];
let out = runs_to_ansi(&runs, theme::default_theme(), theme::Capability::None, true);
assert_eq!(out, "\x1b]8;;https://example.com\x1b\\click\x1b]8;;\x1b\\");
}
#[test]
fn osc8_pair_balanced_when_hyperlinked_run_is_truncated() {
let mut rendered = RenderedSegment::new("very-long-branch-name")
.with_style(Style::default().with_hyperlink("https://example.com/branch"));
rendered = truncate_to(rendered, 8);
let runs = vec![StyledRun::new(
rendered.text().to_string(),
rendered.style.clone(),
)];
let out = runs_to_ansi(&runs, theme::default_theme(), theme::Capability::None, true);
assert!(
out.starts_with("\x1b]8;;https://example.com/branch\x1b\\"),
"OSC 8 open present: {out:?}"
);
assert!(
out.ends_with("\x1b]8;;\x1b\\"),
"OSC 8 close present: {out:?}"
);
assert!(out.contains('…'), "truncation marker preserved: {out:?}");
assert_eq!(
out.matches("\x1b]8;;").count(),
2,
"exactly one open and one close: {out:?}"
);
}
#[test]
fn osc8_pair_balanced_when_hyperlinked_run_truncated_to_zero() {
let rendered = RenderedSegment::new("anything")
.with_style(Style::default().with_hyperlink("https://example.com"));
let truncated = truncate_to(rendered, 0);
let runs = vec![StyledRun::new(
truncated.text().to_string(),
truncated.style.clone(),
)];
let out = runs_to_ansi(&runs, theme::default_theme(), theme::Capability::None, true);
assert_eq!(
out, "\x1b]8;;https://example.com\x1b\\\x1b]8;;\x1b\\",
"empty-text run still emits balanced OSC 8 pair: {out:?}"
);
}
#[test]
fn runs_to_ansi_emits_independent_osc8_pairs_for_adjacent_hyperlinked_runs() {
let runs = vec![
StyledRun::new("a", Style::default().with_hyperlink("https://a.example")),
StyledRun::new("b", Style::default().with_hyperlink("https://b.example")),
];
let out = runs_to_ansi(&runs, theme::default_theme(), theme::Capability::None, true);
assert_eq!(
out,
"\x1b]8;;https://a.example\x1b\\a\x1b]8;;\x1b\\\x1b]8;;https://b.example\x1b\\b\x1b]8;;\x1b\\"
);
assert_eq!(out.matches("\x1b]8;;").count(), 4, "two opens + two closes");
}
#[test]
fn push_osc8_open_strips_control_chars_from_url() {
let runs = vec![StyledRun::new(
"x",
Style::default().with_hyperlink("https://example.com\x1b\\evil\x07more"),
)];
let out = runs_to_ansi(&runs, theme::default_theme(), theme::Capability::None, true);
assert_eq!(
out.matches("\x1b]8;;").count(),
2,
"exactly one pair: {out:?}"
);
assert!(!out.contains("\x1b\\evil"), "ESC \\ stripped: {out:?}");
assert!(!out.contains('\x07'), "BEL stripped: {out:?}");
assert!(
out.contains("https://example.com\\evilmore"),
"non-control chars survive: {out:?}"
);
}
#[test]
fn push_osc8_open_strips_c1_string_terminator_and_nul() {
let runs = vec![StyledRun::new(
"x",
Style::default().with_hyperlink("https://a.example\x00b\x7fc\u{009C}d"),
)];
let out = runs_to_ansi(&runs, theme::default_theme(), theme::Capability::None, true);
assert_eq!(out.matches("\x1b]8;;").count(), 2, "single pair: {out:?}");
assert!(!out.contains('\x00'), "NUL stripped: {out:?}");
assert!(!out.contains('\x7f'), "DEL stripped: {out:?}");
assert!(!out.contains('\u{009C}'), "C1 ST stripped: {out:?}");
assert!(out.contains("https://a.examplebcd"));
}
#[test]
fn runs_to_ansi_capability_none_emits_unwrapped_text() {
use crate::theme::Role;
let runs = vec![
StyledRun::new("plain", Style::default()),
StyledRun::new(" ", Style::default()),
StyledRun::new("warn", Style::role(Role::Warning)),
];
let out = runs_to_ansi(
&runs,
theme::default_theme(),
theme::Capability::None,
false,
);
assert_eq!(out, "plain warn");
assert!(!out.contains('\x1b'), "unexpected ANSI escape: {out:?}");
}
struct DroppableStub(&'static str);
impl Segment for DroppableStub {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new(self.0)))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(200)
}
}
#[test]
fn try_reflow_preserves_segment_id_reference() {
static ALT_ID: Cow<'static, str> = Cow::Borrowed("alt");
let entry = SegmentEntry {
id: &ALT_ID,
rendered: RenderedSegment::new("workspace-with-extra-content"),
defaults: SegmentDefaults::with_priority(100).with_truncatable(true),
segment: noop_segment(),
};
let original_id_ptr = std::ptr::from_ref(entry.id);
let reflowed =
super::try_reflow(&entry, 10).expect("reflow must succeed at target 18 / floor 2");
assert!(
std::ptr::eq(std::ptr::from_ref(reflowed.id), original_id_ptr),
"try_reflow must preserve the id reference, not clone it",
);
assert_eq!(
reflowed.id.as_ref(),
"alt",
"id content must survive — defense-in-depth alongside ptr::eq",
);
}
macro_rules! let_capturing_observers {
($name:ident, $decisions:ident) => {
let mut __warn_for_capture: fn(&str) = |_: &str| {};
let mut $decisions: Vec<LayoutDecision> = Vec::new();
let mut __on_decision_for_capture = |d: &LayoutDecision| $decisions.push(d.clone());
let mut $name = LayoutObservers::new(&mut __warn_for_capture)
.with_decision(&mut __on_decision_for_capture);
};
}
#[test]
fn emit_priority_drop_when_segment_dropped_under_pressure() {
let items: Vec<LineItem> = vec![
LineItem::Segment {
id: Cow::Borrowed("anchor"),
segment: Box::new(StubSegment(Ok(Some(RenderedSegment::new("a"))))),
},
LineItem::Separator(Separator::Space),
LineItem::Segment {
id: Cow::Borrowed("droppable"),
segment: Box::new(DroppableStub("zzzzzz")),
},
];
let_capturing_observers!(observers, decisions);
let _ = render_to_runs(&items, &empty_ctx(), 1, &mut observers);
assert_eq!(decisions.len(), 1, "exactly one decision: {decisions:?}");
match &decisions[0] {
LayoutDecision::PriorityDrop {
id,
priority,
terminal_width,
overflow,
dropped_width,
..
} => {
assert_eq!(id.as_ref(), "droppable");
assert_eq!(*priority, 200);
assert_eq!(*terminal_width, 1);
assert!(*overflow >= 1);
assert_eq!(*dropped_width, 6);
}
other => panic!("expected PriorityDrop, got {other:?}"),
}
}
#[test]
fn emit_shrink_applied_when_shrink_to_fit_succeeds() {
let items: Vec<LineItem> = vec![
LineItem::Segment {
id: Cow::Borrowed("shrinkable"),
segment: Box::new(ShrinkableSegment {
full: "longbranch * ↑2 ↓1",
compact: "longbranch",
}),
},
LineItem::Separator(Separator::Space),
LineItem::Segment {
id: Cow::Borrowed("anchor"),
segment: Box::new(AnchorSegment("KEEP")),
},
];
let_capturing_observers!(observers, decisions);
let _ = render_to_runs(&items, &empty_ctx(), 17, &mut observers);
assert_eq!(decisions.len(), 1, "exactly one decision: {decisions:?}");
match &decisions[0] {
LayoutDecision::ShrinkApplied {
id,
from,
to,
target,
..
} => {
assert_eq!(id.as_ref(), "shrinkable");
assert_eq!(*from, 18);
assert_eq!(*to, 10);
assert_eq!(*target, 12);
}
other => panic!("expected ShrinkApplied, got {other:?}"),
}
}
#[test]
fn emit_reflow_applied_when_truncatable_segment_end_ellipsis_fits() {
struct TruncatableStub(&'static str);
impl Segment for TruncatableStub {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new(self.0)))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(200).with_truncatable(true)
}
}
let items: Vec<LineItem> = vec![
LineItem::Segment {
id: Cow::Borrowed("reflowed"),
segment: Box::new(TruncatableStub("workspace-very-long-name")),
},
LineItem::Separator(Separator::Space),
LineItem::Segment {
id: Cow::Borrowed("anchor"),
segment: Box::new(AnchorSegment("X")),
},
];
let_capturing_observers!(observers, decisions);
let _ = render_to_runs(&items, &empty_ctx(), 10, &mut observers);
assert_eq!(decisions.len(), 1, "exactly one decision: {decisions:?}");
match &decisions[0] {
LayoutDecision::ReflowApplied {
id,
from,
to,
target,
..
} => {
assert_eq!(id.as_ref(), "reflowed");
assert_eq!(*from, 24);
assert!(*to <= *target);
assert_eq!(*target, 8);
}
other => panic!("expected ReflowApplied, got {other:?}"),
}
}
#[test]
fn emit_width_bound_under_min_drop_when_render_below_min_floor() {
struct NarrowSegment;
impl Segment for NarrowSegment {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("abc")))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(10)
.with_width(WidthBounds::new(10, u16::MAX).expect("valid"))
}
}
let items: Vec<LineItem> = vec![LineItem::Segment {
id: Cow::Borrowed("narrow"),
segment: Box::new(NarrowSegment),
}];
let_capturing_observers!(observers, decisions);
let _ = render_to_runs(&items, &empty_ctx(), 100, &mut observers);
assert_eq!(decisions.len(), 1, "exactly one decision: {decisions:?}");
match &decisions[0] {
LayoutDecision::WidthBoundUnderMinDrop {
id,
rendered_width,
min,
..
} => {
assert_eq!(id.as_ref(), "narrow");
assert_eq!(*rendered_width, 3);
assert_eq!(*min, 10);
}
other => panic!("expected WidthBoundUnderMinDrop, got {other:?}"),
}
}
#[test]
fn emit_width_bound_over_max_truncate_when_render_above_max() {
struct WideSegment;
impl Segment for WideSegment {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("abcdefghij")))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(10).with_width(WidthBounds::new(0, 5).expect("valid"))
}
}
let items: Vec<LineItem> = vec![LineItem::Segment {
id: Cow::Borrowed("wide"),
segment: Box::new(WideSegment),
}];
let_capturing_observers!(observers, decisions);
let _ = render_to_runs(&items, &empty_ctx(), 100, &mut observers);
assert_eq!(decisions.len(), 1, "exactly one decision: {decisions:?}");
match &decisions[0] {
LayoutDecision::WidthBoundOverMaxTruncate {
id,
rendered_width,
max,
..
} => {
assert_eq!(id.as_ref(), "wide");
assert_eq!(*rendered_width, 10);
assert_eq!(*max, 5);
}
other => panic!("expected WidthBoundOverMaxTruncate, got {other:?}"),
}
}
#[test]
fn no_decisions_emitted_when_no_pressure_no_bounds() {
let items = line_items_spaced(vec![
Box::new(StubSegment(Ok(Some(RenderedSegment::new("a"))))),
Box::new(StubSegment(Ok(Some(RenderedSegment::new("b"))))),
]);
let_capturing_observers!(observers, decisions);
let _ = render_to_runs(&items, &empty_ctx(), 100, &mut observers);
assert!(
decisions.is_empty(),
"no decisions on happy path: {decisions:?}"
);
}
#[test]
fn emit_priority_drop_via_truncatable_path_when_reflow_target_below_floor() {
struct FloorBoundTruncatable;
impl Segment for FloorBoundTruncatable {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("abcdefghij"))) }
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(200)
.with_truncatable(true)
.with_width(WidthBounds::new(8, u16::MAX).expect("valid"))
}
}
let items: Vec<LineItem> = vec![
LineItem::Segment {
id: Cow::Borrowed("anchor"),
segment: Box::new(StubSegment(Ok(Some(RenderedSegment::new("a"))))),
},
LineItem::Separator(Separator::Space),
LineItem::Segment {
id: Cow::Borrowed("floor-bound"),
segment: Box::new(FloorBoundTruncatable),
},
];
let_capturing_observers!(observers, decisions);
let _ = render_to_runs(&items, &empty_ctx(), 6, &mut observers);
assert_eq!(decisions.len(), 1, "exactly one decision: {decisions:?}");
match &decisions[0] {
LayoutDecision::PriorityDrop {
id,
priority,
terminal_width,
overflow,
dropped_width,
..
} => {
assert_eq!(id.as_ref(), "floor-bound");
assert_eq!(*priority, 200);
assert_eq!(*terminal_width, 6);
assert!(*overflow >= 1);
assert_eq!(*dropped_width, 10);
}
other => panic!("expected PriorityDrop, got {other:?}"),
}
}
#[test]
fn emit_multiple_decisions_in_iteration_order_under_compound_pressure() {
struct DroppableP150(&'static str);
impl Segment for DroppableP150 {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new(self.0)))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(150)
}
}
let items: Vec<LineItem> = vec![
LineItem::Segment {
id: Cow::Borrowed("anchor"),
segment: Box::new(AnchorSegment("A")),
},
LineItem::Separator(Separator::Space),
LineItem::Segment {
id: Cow::Borrowed("shrinkable"),
segment: Box::new(ShrinkableSegment {
full: "longbranch * ↑2 ↓1",
compact: "longbranch",
}),
},
LineItem::Separator(Separator::Space),
LineItem::Segment {
id: Cow::Borrowed("droppable"),
segment: Box::new(DroppableP150("midpriority")),
},
];
let_capturing_observers!(observers, decisions);
let _ = render_to_runs(&items, &empty_ctx(), 11, &mut observers);
assert_eq!(decisions.len(), 2, "two decisions in order: {decisions:?}");
match (&decisions[0], &decisions[1]) {
(
LayoutDecision::PriorityDrop {
id: id1,
priority: p1,
..
},
LayoutDecision::PriorityDrop {
id: id2,
priority: p2,
..
},
) => {
assert_eq!(
id1.as_ref(),
"shrinkable",
"shrinkable drops first (priority 200)"
);
assert_eq!(*p1, 200);
assert_eq!(
id2.as_ref(),
"droppable",
"droppable drops second (priority 150)"
);
assert_eq!(*p2, 150);
}
other => panic!("expected two PriorityDrops in order, got {other:?}"),
}
}
#[test]
fn emit_priority_drop_does_not_panic_on_zero_width_segment() {
struct EmptySegment;
impl Segment for EmptySegment {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("")))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(200)
}
}
let items: Vec<LineItem> = vec![
LineItem::Segment {
id: Cow::Borrowed("anchor-l"),
segment: Box::new(AnchorSegment("L")),
},
LineItem::Separator(Separator::Space),
LineItem::Segment {
id: Cow::Borrowed("empty"),
segment: Box::new(EmptySegment),
},
LineItem::Separator(Separator::Space),
LineItem::Segment {
id: Cow::Borrowed("anchor-r"),
segment: Box::new(AnchorSegment("R")),
},
];
let_capturing_observers!(observers, decisions);
let _ = render_to_runs(&items, &empty_ctx(), 2, &mut observers);
assert_eq!(decisions.len(), 1, "exactly one decision: {decisions:?}");
match &decisions[0] {
LayoutDecision::PriorityDrop {
id, dropped_width, ..
} => {
assert_eq!(id.as_ref(), "empty");
assert_eq!(*dropped_width, 0, "zero-width drop must round-trip cleanly");
}
other => panic!("expected PriorityDrop, got {other:?}"),
}
}