use crate::data_context::{DataContext, DataDep};
use crate::theme::{Role, Style};
use std::borrow::Cow;
use unicode_width::UnicodeWidthStr;
pub mod agent;
pub mod builder;
pub mod context_bar;
pub mod context_window;
pub mod cost;
pub mod effort;
pub mod extra_usage;
pub mod extras;
pub mod git_branch;
pub mod model;
pub mod output_style;
pub mod rate_limit;
pub mod session_duration;
pub mod tokens;
pub mod version;
pub mod vim;
pub mod workspace;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RenderedSegment {
pub(crate) text: String,
pub(crate) width: u16,
pub(crate) right_separator: Option<Separator>,
pub(crate) style: Style,
}
impl RenderedSegment {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
let text = sanitize_control_chars(text.into());
let width = text_width(&text);
Self {
text,
width,
right_separator: None,
style: Style::default(),
}
}
#[must_use]
pub fn with_separator(text: impl Into<String>, separator: Separator) -> Self {
let text = sanitize_control_chars(text.into());
let width = text_width(&text);
Self {
text,
width,
right_separator: Some(separator),
style: Style::default(),
}
}
#[must_use]
pub fn with_role(mut self, role: Role) -> Self {
self.style.role = Some(role);
self
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn style(&self) -> &Style {
&self.style
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn width(&self) -> u16 {
self.width
}
#[must_use]
pub fn right_separator(&self) -> Option<&Separator> {
self.right_separator.as_ref()
}
#[must_use]
pub(crate) fn from_parts(
text: String,
width: u16,
right_separator: Option<Separator>,
style: Style,
) -> Self {
Self {
text,
width,
right_separator,
style,
}
}
}
#[must_use]
pub(crate) fn text_width(s: &str) -> u16 {
u16::try_from(UnicodeWidthStr::width(s)).unwrap_or(u16::MAX)
}
pub(crate) fn sanitize_control_chars(s: String) -> String {
if !s.chars().any(char::is_control) {
return s;
}
s.chars().filter(|c| !c.is_control()).collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Separator {
Space,
Theme,
Literal(Cow<'static, str>),
Powerline { width: PowerlineWidth },
None,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum PowerlineWidth {
#[default]
One,
Two,
}
impl PowerlineWidth {
#[must_use]
pub const fn cells(self) -> u16 {
match self {
Self::One => 1,
Self::Two => 2,
}
}
}
const POWERLINE_CHEVRON_PADDED: &str = " \u{E0B0} ";
impl Separator {
#[must_use]
pub const fn powerline() -> Self {
Self::Powerline {
width: PowerlineWidth::One,
}
}
#[must_use]
pub fn text(&self) -> &str {
match self {
Self::Space | Self::Theme => " ",
Self::Literal(s) => s,
Self::Powerline { .. } => POWERLINE_CHEVRON_PADDED,
Self::None => "",
}
}
#[must_use]
pub fn width(&self) -> u16 {
match self {
Self::Space | Self::Theme => 1,
Self::Literal(s) => text_width(s),
Self::Powerline { width } => width.cells() + 2,
Self::None => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WidthBounds {
min: u16,
max: u16,
}
impl WidthBounds {
#[must_use]
pub fn new(min: u16, max: u16) -> Option<Self> {
(min <= max).then_some(Self { min, max })
}
#[must_use]
pub fn min(self) -> u16 {
self.min
}
#[must_use]
pub fn max(self) -> u16 {
self.max
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct SegmentDefaults {
pub priority: u8,
pub width: Option<WidthBounds>,
pub truncatable: bool,
}
impl SegmentDefaults {
#[must_use]
pub fn with_priority(priority: u8) -> Self {
Self {
priority,
..Self::default()
}
}
#[must_use]
pub fn with_width(mut self, bounds: WidthBounds) -> Self {
self.width = Some(bounds);
self
}
#[must_use]
pub fn with_truncatable(mut self, truncatable: bool) -> Self {
self.truncatable = truncatable;
self
}
}
impl Default for SegmentDefaults {
fn default() -> Self {
Self {
priority: 128,
width: None,
truncatable: false,
}
}
}
pub type RenderResult = Result<Option<RenderedSegment>, SegmentError>;
#[derive(Debug)]
#[non_exhaustive]
pub struct SegmentError {
pub message: String,
pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl SegmentError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}
#[must_use]
pub fn with_source(
message: impl Into<String>,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self {
message: message.into(),
source: Some(source),
}
}
}
impl std::fmt::Display for SegmentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)?;
if let Some(src) = &self.source {
write!(f, ": {src}")?;
}
Ok(())
}
}
impl std::error::Error for SegmentError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_deref().map(|e| e as &dyn std::error::Error)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct RenderContext {
pub terminal_width: u16,
}
impl RenderContext {
#[must_use]
pub fn new(terminal_width: u16) -> Self {
Self { terminal_width }
}
}
pub trait Segment: Send {
fn render(&self, ctx: &DataContext, rc: &RenderContext) -> RenderResult;
#[allow(unused_variables)]
#[must_use]
fn shrink_to_fit(
&self,
ctx: &DataContext,
rc: &RenderContext,
target: u16,
) -> Option<RenderedSegment> {
None
}
#[must_use]
fn data_deps(&self) -> &'static [DataDep] {
&[DataDep::Status]
}
#[must_use]
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::default()
}
}
pub const DEFAULT_SEGMENT_IDS: &[&str] = &[
"model",
"context_window",
"cost",
"effort",
"git_branch",
"workspace",
];
pub const BUILT_IN_SEGMENT_IDS: &[&str] = &[
"model",
"context_window",
"context_bar",
"workspace",
"cost",
"effort",
"output_style",
"vim",
"agent",
"git_branch",
"rate_limit_5h",
"rate_limit_7d",
"rate_limit_5h_reset",
"rate_limit_7d_reset",
"extra_usage",
"session_duration",
"tokens_input",
"tokens_output",
"tokens_cached",
"tokens_total",
"version",
];
#[must_use]
pub fn built_in_by_id(
id: &str,
extras: Option<&std::collections::BTreeMap<String, toml::Value>>,
warn: &mut impl FnMut(&str),
) -> Option<Box<dyn Segment>> {
let empty: std::collections::BTreeMap<String, toml::Value> = std::collections::BTreeMap::new();
let e = extras.unwrap_or(&empty);
match id {
"model" => Some(Box::new(model::ModelSegment::from_extras(e, warn))),
"context_window" => Some(Box::new(context_window::ContextWindowSegment)),
"context_bar" => Some(Box::new(context_bar::ContextBarSegment::from_extras(
e, warn,
))),
"workspace" => Some(Box::new(workspace::WorkspaceSegment)),
"cost" => Some(Box::new(cost::CostSegment)),
"effort" => Some(Box::new(effort::EffortSegment)),
"output_style" => Some(Box::new(output_style::OutputStyleSegment)),
"vim" => Some(Box::new(vim::VimSegment)),
"agent" => Some(Box::new(agent::AgentSegment)),
"git_branch" => Some(Box::new(git_branch::GitBranchSegment::from_extras(e, warn))),
"rate_limit_5h" => Some(Box::new(
rate_limit::five_hour::RateLimit5hSegment::from_extras(e, warn),
)),
"rate_limit_7d" => Some(Box::new(
rate_limit::seven_day::RateLimit7dSegment::from_extras(e, warn),
)),
"rate_limit_5h_reset" => Some(Box::new(
rate_limit::five_hour::RateLimit5hResetSegment::from_extras(e, warn),
)),
"rate_limit_7d_reset" => Some(Box::new(
rate_limit::seven_day::RateLimit7dResetSegment::from_extras(e, warn),
)),
"extra_usage" => Some(Box::new(extra_usage::ExtraUsageSegment::from_extras(
e, warn,
))),
"session_duration" => Some(Box::new(session_duration::SessionDurationSegment)),
"tokens_input" => Some(Box::new(tokens::TokensInputSegment)),
"tokens_output" => Some(Box::new(tokens::TokensOutputSegment)),
"tokens_cached" => Some(Box::new(tokens::TokensCachedSegment)),
"tokens_total" => Some(Box::new(tokens::TokensTotalSegment)),
"version" => Some(Box::new(version::VersionSegment::from_extras(e, warn))),
_ => None,
}
}
pub struct OverriddenSegment {
inner: Box<dyn Segment>,
priority: Option<u8>,
width: Option<WidthBounds>,
user_style: Option<Style>,
}
impl OverriddenSegment {
#[must_use]
pub fn new(inner: Box<dyn Segment>) -> Self {
Self {
inner,
priority: None,
width: None,
user_style: None,
}
}
#[must_use]
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = Some(priority);
self
}
#[must_use]
pub fn with_width(mut self, bounds: WidthBounds) -> Self {
self.width = Some(bounds);
self
}
#[must_use]
pub fn with_user_style(mut self, style: Style) -> Self {
self.user_style = Some(style);
self
}
}
impl Segment for OverriddenSegment {
fn render(&self, ctx: &DataContext, rc: &RenderContext) -> RenderResult {
let result = self.inner.render(ctx, rc)?;
Ok(result.map(|r| match &self.user_style {
Some(override_style) => {
let merged = merge_user_override(r.style(), override_style);
r.with_style(merged)
}
None => r,
}))
}
fn shrink_to_fit(
&self,
ctx: &DataContext,
rc: &RenderContext,
target: u16,
) -> Option<RenderedSegment> {
let inner = self.inner.shrink_to_fit(ctx, rc, target)?;
Some(match &self.user_style {
Some(override_style) => {
let merged = merge_user_override(inner.style(), override_style);
inner.with_style(merged)
}
None => inner,
})
}
fn data_deps(&self) -> &'static [DataDep] {
self.inner.data_deps()
}
fn defaults(&self) -> SegmentDefaults {
let mut d = self.inner.defaults();
if let Some(p) = self.priority {
d.priority = p;
}
if let Some(w) = self.width {
d.width = Some(w);
}
d
}
}
fn merge_user_override(inner: &Style, override_style: &Style) -> Style {
let mut merged = override_style.clone();
if merged.hyperlink.is_none() {
merged.hyperlink = inner.hyperlink.clone();
}
merged
}
#[non_exhaustive]
pub enum LineItem {
Segment {
id: std::borrow::Cow<'static, str>,
segment: Box<dyn Segment>,
},
Separator(Separator),
}
impl std::fmt::Debug for LineItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Segment { id, segment } => f
.debug_struct("Segment")
.field("id", id)
.field("defaults", &segment.defaults())
.finish(),
Self::Separator(sep) => f.debug_tuple("Separator").field(sep).finish(),
}
}
}
#[cfg(test)]
mod layout_type_tests {
use super::*;
#[test]
fn rendered_segment_computes_width() {
let r = RenderedSegment::new("hello");
assert_eq!(r.text(), "hello");
assert_eq!(r.width(), 5);
assert_eq!(r.right_separator(), None);
}
#[test]
fn rendered_segment_counts_cells_not_bytes_for_middle_dot() {
let r = RenderedSegment::new("42% · 200k");
assert_eq!(r.width(), 10);
}
#[test]
fn rendered_segment_strips_csi_clear_screen_injection() {
let r = RenderedSegment::new("evil\x1b[2J");
assert_eq!(r.text(), "evil[2J");
assert_eq!(r.width(), 7);
assert!(!r.text().contains('\x1b'));
}
#[test]
fn rendered_segment_strips_osc_set_title_with_bel_terminator() {
let r = RenderedSegment::new("\x1b]0;pwn\x07rest");
assert_eq!(r.text(), "]0;pwnrest");
assert!(!r.text().contains('\x1b'));
assert!(!r.text().contains('\x07'));
}
#[test]
fn rendered_segment_strips_common_c0_controls() {
let r = RenderedSegment::new("a\x07b\x08c\td\ne\rf");
assert_eq!(r.text(), "abcdef");
assert_eq!(r.width(), 6);
}
#[test]
fn rendered_segment_strips_c1_controls_and_del() {
let r = RenderedSegment::new("x\u{007F}y\u{0085}z\u{009B}");
assert_eq!(r.text(), "xyz");
assert_eq!(r.width(), 3);
}
#[test]
fn rendered_segment_preserves_unicode_without_controls() {
let r = RenderedSegment::new("café · 日本語");
assert_eq!(r.text(), "café · 日本語");
}
#[test]
fn rendered_segment_empty_string_stays_empty() {
let r = RenderedSegment::new("");
assert_eq!(r.text(), "");
assert_eq!(r.width(), 0);
}
#[test]
fn rendered_segment_all_control_input_collapses_to_empty() {
let r = RenderedSegment::new("\x1b\x07\n\t");
assert_eq!(r.text(), "");
assert_eq!(r.width(), 0);
}
#[test]
fn rendered_segment_with_separator_also_strips_controls() {
let r = RenderedSegment::with_separator("hi\x1bthere", Separator::None);
assert_eq!(r.text(), "hithere");
assert_eq!(r.width(), 7);
}
#[test]
fn rendered_segment_with_separator_exposes_override() {
let r = RenderedSegment::with_separator("x", Separator::None);
assert_eq!(r.right_separator(), Some(&Separator::None));
}
#[test]
fn separator_widths_match_expected() {
assert_eq!(Separator::Space.width(), 1);
assert_eq!(Separator::Theme.width(), 1);
assert_eq!(Separator::None.width(), 0);
assert_eq!(Separator::Literal(Cow::Borrowed(" | ")).width(), 3);
assert_eq!(Separator::powerline().width(), 3);
assert_eq!(
Separator::Powerline {
width: PowerlineWidth::Two,
}
.width(),
4
);
}
#[test]
fn width_bounds_rejects_inverted_range() {
assert!(WidthBounds::new(20, 10).is_none());
assert!(WidthBounds::new(10, 10).is_some());
assert!(WidthBounds::new(0, u16::MAX).is_some());
}
#[test]
fn line_item_debug_renders_each_variant() {
struct StubSeg;
impl Segment for StubSeg {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(None)
}
}
let seg = LineItem::Segment {
id: std::borrow::Cow::Borrowed("stub"),
segment: Box::new(StubSeg),
};
let sep = LineItem::Separator(Separator::powerline());
let seg_dbg = format!("{seg:?}");
let sep_dbg = format!("{sep:?}");
assert!(seg_dbg.starts_with("Segment {"), "got {seg_dbg:?}");
assert!(sep_dbg.starts_with("Separator("), "got {sep_dbg:?}");
assert!(seg_dbg.contains("id:"), "got {seg_dbg:?}");
assert!(seg_dbg.contains("defaults:"), "got {seg_dbg:?}");
assert!(seg_dbg.contains("stub"), "got {seg_dbg:?}");
assert!(seg_dbg.contains("priority"), "got {seg_dbg:?}");
}
#[test]
fn segment_defaults_default_priority_is_128() {
assert_eq!(SegmentDefaults::default().priority, 128);
}
#[test]
fn with_priority_preserves_other_defaults() {
let d = SegmentDefaults::with_priority(64);
assert_eq!(d.priority, 64);
assert_eq!(d.width, None);
assert!(!d.truncatable);
}
#[test]
fn builders_chain_on_segment_defaults() {
let bounds = WidthBounds::new(4, 40).expect("valid bounds");
let d = SegmentDefaults::with_priority(32)
.with_width(bounds)
.with_truncatable(true);
assert_eq!(d.priority, 32);
assert_eq!(d.width, Some(bounds));
assert!(d.truncatable);
}
#[test]
fn segment_error_display_includes_message_only_without_source() {
let err = SegmentError::new("missing rate_limits field");
assert_eq!(err.to_string(), "missing rate_limits field");
}
#[test]
fn segment_error_display_chains_source() {
let src = std::io::Error::new(std::io::ErrorKind::NotFound, "cache.json");
let err = SegmentError::with_source("cache read failed", Box::new(src));
let rendered = err.to_string();
assert!(rendered.starts_with("cache read failed: "));
assert!(rendered.contains("cache.json"));
}
#[test]
fn segment_error_source_chain_is_walkable() {
use std::error::Error;
let src = std::io::Error::other("inner");
let err = SegmentError::with_source("outer", Box::new(src));
let source = err.source().expect("source present");
assert_eq!(source.to_string(), "inner");
}
#[test]
fn built_in_by_id_resolves_every_default_segment() {
for id in DEFAULT_SEGMENT_IDS {
assert!(
built_in_by_id(id, None, &mut |_| {}).is_some(),
"expected built-in registry to know {id}"
);
}
}
#[test]
fn built_in_by_id_resolves_additional_documented_ids() {
for id in [
"context_bar",
"session_duration",
"rate_limit_5h",
"rate_limit_7d",
"rate_limit_5h_reset",
"rate_limit_7d_reset",
"extra_usage",
"tokens_input",
"tokens_output",
"tokens_cached",
"tokens_total",
"output_style",
"vim",
"agent",
] {
assert!(
built_in_by_id(id, None, &mut |_| {}).is_some(),
"expected {id} to resolve"
);
}
}
#[test]
fn built_in_by_id_resolves_every_id_in_built_in_segment_ids() {
for id in BUILT_IN_SEGMENT_IDS {
assert!(
built_in_by_id(id, None, &mut |_| {}).is_some(),
"BUILT_IN_SEGMENT_IDS lists {id} but built_in_by_id can't construct it"
);
}
}
#[test]
fn built_in_by_id_rejects_unknown() {
assert!(built_in_by_id("nope", None, &mut |_| {}).is_none());
assert!(built_in_by_id("", None, &mut |_| {}).is_none());
}
#[test]
fn built_in_by_id_threads_extras_to_version_segment() {
use crate::input::{ModelInfo, StatusContext, Tool, WorkspaceInfo};
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
let mut extras = BTreeMap::new();
extras.insert("prefix".to_string(), toml::Value::String("CC ".to_string()));
let seg = built_in_by_id("version", Some(&extras), &mut |_| {})
.expect("version segment resolves");
let ctx = DataContext::new(StatusContext {
tool: Tool::ClaudeCode,
model: Some(ModelInfo {
display_name: "X".into(),
}),
workspace: Some(WorkspaceInfo {
project_dir: PathBuf::from("/r"),
git_worktree: None,
}),
context_window: None,
cost: None,
effort: None,
vim: None,
output_style: None,
agent_name: None,
version: Some("2.1.90".into()),
raw: Arc::new(serde_json::Value::Null),
});
let rc = RenderContext::new(80);
let rendered = seg.render(&ctx, &rc).unwrap().expect("renders");
assert_eq!(rendered.text(), "CC 2.1.90");
}
#[test]
fn overridden_segment_replaces_priority() {
let base = built_in_by_id("workspace", None, &mut |_| {}).expect("known id");
let base_priority = base.defaults().priority;
let wrapped = OverriddenSegment::new(base).with_priority(200);
assert_eq!(wrapped.defaults().priority, 200);
assert_ne!(wrapped.defaults().priority, base_priority);
}
#[test]
fn overridden_segment_replaces_width_bounds() {
let base = built_in_by_id("workspace", None, &mut |_| {}).expect("known id");
assert_eq!(base.defaults().width, None);
let bounds = WidthBounds::new(5, 40).expect("valid");
let wrapped = OverriddenSegment::new(base).with_width(bounds);
assert_eq!(wrapped.defaults().width, Some(bounds));
}
#[test]
fn overridden_segment_delegates_render_to_inner() {
let wrapped =
OverriddenSegment::new(built_in_by_id("workspace", None, &mut |_| {}).unwrap())
.with_priority(0);
let rendered = wrapped
.render(&stub_ctx(), &stub_rc())
.unwrap()
.expect("rendered");
assert_eq!(rendered.text(), "linesmith");
}
#[test]
fn style_override_wholesale_replaces_inner_declared_style() {
struct Styled;
impl Segment for Styled {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(
RenderedSegment::new("x")
.with_role(Role::Accent)
.with_style(Style {
bold: true,
..Style::default()
}),
))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(0)
}
}
let override_style = Style {
role: Some(Role::Primary),
italic: true,
..Style::default()
};
let wrapped =
OverriddenSegment::new(Box::new(Styled)).with_user_style(override_style.clone());
let rendered = wrapped
.render(&stub_ctx(), &stub_rc())
.unwrap()
.expect("rendered");
assert_eq!(rendered.style, override_style);
}
#[test]
fn user_style_override_preserves_inner_hyperlink() {
struct Linked;
impl Segment for Linked {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("x").with_style(
Style::default().with_hyperlink("https://example.com"),
)))
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(0)
}
}
let override_style = Style::role(Role::Error);
let wrapped =
OverriddenSegment::new(Box::new(Linked)).with_user_style(override_style.clone());
let rendered = wrapped
.render(&stub_ctx(), &stub_rc())
.unwrap()
.expect("rendered");
assert_eq!(rendered.style.role, Some(Role::Error));
assert_eq!(
rendered.style.hyperlink.as_deref(),
Some("https://example.com"),
);
}
#[test]
fn style_override_preserves_inner_none_return() {
struct Hidden;
impl Segment for Hidden {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(None)
}
fn defaults(&self) -> SegmentDefaults {
SegmentDefaults::with_priority(0)
}
}
let wrapped =
OverriddenSegment::new(Box::new(Hidden)).with_user_style(Style::role(Role::Primary));
assert_eq!(wrapped.render(&stub_ctx(), &stub_rc()).unwrap(), None);
}
#[test]
fn shrink_to_fit_passthrough_reaches_inner_with_user_style_applied() {
struct Shrinkable;
impl Segment for Shrinkable {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("full")))
}
fn shrink_to_fit(
&self,
_: &DataContext,
_: &RenderContext,
target: u16,
) -> Option<RenderedSegment> {
let r = RenderedSegment::new("c");
(r.width <= target).then_some(r)
}
}
let override_style = Style {
role: Some(Role::Primary),
italic: true,
..Style::default()
};
let wrapped =
OverriddenSegment::new(Box::new(Shrinkable)).with_user_style(override_style.clone());
let shrunk = wrapped
.shrink_to_fit(&stub_ctx(), &stub_rc(), 5)
.expect("inner returned compact form");
assert_eq!(shrunk.text, "c");
assert_eq!(shrunk.style, override_style);
}
#[test]
fn shrink_to_fit_passthrough_keeps_inner_style_when_no_user_override() {
struct ShrinkableWithRole;
impl Segment for ShrinkableWithRole {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("full").with_role(Role::Accent)))
}
fn shrink_to_fit(
&self,
_: &DataContext,
_: &RenderContext,
_target: u16,
) -> Option<RenderedSegment> {
Some(RenderedSegment::new("c").with_role(Role::Accent))
}
}
let wrapped = OverriddenSegment::new(Box::new(ShrinkableWithRole));
let shrunk = wrapped
.shrink_to_fit(&stub_ctx(), &stub_rc(), 10)
.expect("inner returned compact form");
assert_eq!(shrunk.style.role, Some(Role::Accent));
}
#[test]
fn shrink_to_fit_passthrough_returns_none_when_inner_declines() {
struct Plain;
impl Segment for Plain {
fn render(&self, _: &DataContext, _: &RenderContext) -> RenderResult {
Ok(Some(RenderedSegment::new("plain")))
}
}
let wrapped =
OverriddenSegment::new(Box::new(Plain)).with_user_style(Style::role(Role::Primary));
assert!(wrapped
.shrink_to_fit(&stub_ctx(), &stub_rc(), 100)
.is_none());
}
fn stub_ctx() -> DataContext {
use crate::input::{ModelInfo, StatusContext, Tool, WorkspaceInfo};
use std::path::PathBuf;
use std::sync::Arc;
DataContext::new(StatusContext {
tool: Tool::ClaudeCode,
model: Some(ModelInfo {
display_name: "Claude".into(),
}),
workspace: Some(WorkspaceInfo {
project_dir: PathBuf::from("/repo/linesmith"),
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 stub_rc() -> RenderContext {
RenderContext::new(80)
}
}