use ratatui::style::Style;
use ratatui::text::Span;
use super::context::RowContext;
use super::parser::{Token, TokenId};
use crate::tmux_style::apply_tmux_directives;
pub fn render_line(ctx: &RowContext, tokens: &[Token], width: usize) -> Vec<Span<'static>> {
if width == 0 {
return Vec::new();
}
let fill_pos = tokens.iter().position(|t| matches!(t, Token::Fill));
let (left_tokens, right_tokens) = match fill_pos {
Some(pos) => (&tokens[..pos], &tokens[pos + 1..]),
None => (tokens, &[][..]),
};
let left_info: Vec<TokenInfo> = left_tokens.iter().map(|t| TokenInfo::new(t, ctx)).collect();
let right_info: Vec<TokenInfo> = right_tokens
.iter()
.map(|t| TokenInfo::new(t, ctx))
.collect();
let left_info = collapse_empty_fields(left_info);
let right_info = collapse_empty_fields(right_info);
let right_width: usize = right_info.iter().map(|i| i.natural_width).sum();
let left_fixed_width: usize = left_info
.iter()
.filter(|i| !i.is_flex)
.map(|i| i.natural_width)
.sum();
let mut available = width.saturating_sub(right_width + left_fixed_width);
if available == 0 && right_width > 0 && right_width + left_fixed_width > width {
let mut dropped_right_width = right_width;
let mut right_kept: Vec<&TokenInfo> = right_info.iter().collect();
loop {
while let Some(last) = right_kept.last() {
if !last.is_field {
dropped_right_width -= last.natural_width;
right_kept.pop();
} else {
break;
}
}
let Some(last) = right_kept.last() else { break };
if !last.is_field {
break;
}
dropped_right_width -= last.natural_width;
right_kept.pop();
while let Some(last) = right_kept.last() {
if !last.is_field {
dropped_right_width -= last.natural_width;
right_kept.pop();
} else {
break;
}
}
available = width.saturating_sub(dropped_right_width + left_fixed_width);
if available > 0 || dropped_right_width + left_fixed_width <= width {
break;
}
}
let right_info: Vec<TokenInfo> = right_kept.into_iter().cloned().collect();
return render_with_layout(ctx, &left_info, &right_info, width, available);
}
render_with_layout(ctx, &left_info, &right_info, width, available)
}
pub fn is_empty_line(tokens: &[Token], ctx: &RowContext) -> bool {
let mut has_content = false;
for token in tokens {
match token {
Token::Literal(s) => {
if s.trim().is_empty() {
continue;
}
return false;
}
Token::Fill | Token::Style(_) => {}
Token::Field(id) => {
if is_git_segment(*id) {
if ctx.natural_width(*id) > 0 {
has_content = true;
}
} else {
let text = ctx.resolve(*id);
if !text.is_empty() {
has_content = true;
}
}
}
}
}
!has_content
}
#[derive(Clone)]
struct TokenInfo {
token: Token,
natural_width: usize,
is_flex: bool,
is_field: bool,
}
impl TokenInfo {
fn new(token: &Token, ctx: &RowContext) -> Self {
match token {
Token::Literal(s) => Self {
token: Token::Literal(s.clone()),
natural_width: display_width(s),
is_flex: false,
is_field: false,
},
Token::Fill => Self {
token: Token::Fill,
natural_width: 0,
is_flex: false,
is_field: false,
},
Token::Style(s) => Self {
token: Token::Style(s.clone()),
natural_width: 0,
is_flex: false,
is_field: false,
},
Token::Field(id) => Self {
token: Token::Field(*id),
natural_width: ctx.natural_width(*id),
is_flex: id.is_flex(),
is_field: true,
},
}
}
}
fn collapse_empty_fields(infos: Vec<TokenInfo>) -> Vec<TokenInfo> {
let mut keep = vec![true; infos.len()];
for i in 0..infos.len() {
let is_empty_field =
matches!(infos[i].token, Token::Field(_)) && infos[i].natural_width == 0;
if !is_empty_field {
continue;
}
if let Some(j) = next_kept_neighbor(&infos, &keep, i, true)
&& is_whitespace_literal(&infos[j])
{
keep[j] = false;
} else if let Some(j) = next_kept_neighbor(&infos, &keep, i, false)
&& is_whitespace_literal(&infos[j])
{
keep[j] = false;
}
}
infos
.into_iter()
.zip(keep)
.filter_map(|(info, k)| if k { Some(info) } else { None })
.collect()
}
fn next_kept_neighbor(
infos: &[TokenInfo],
keep: &[bool],
from: usize,
forward: bool,
) -> Option<usize> {
if forward {
let mut j = from + 1;
while j < infos.len() {
if keep[j] && !matches!(infos[j].token, Token::Style(_)) {
return Some(j);
}
j += 1;
}
None
} else {
if from == 0 {
return None;
}
let mut j = from - 1;
loop {
if keep[j] && !matches!(infos[j].token, Token::Style(_)) {
return Some(j);
}
if j == 0 {
return None;
}
j -= 1;
}
}
}
fn is_git_segment(id: TokenId) -> bool {
matches!(
id,
TokenId::GitStats | TokenId::GitCommitted | TokenId::GitUncommitted | TokenId::GitRebase
)
}
fn is_whitespace_literal(info: &TokenInfo) -> bool {
if let Token::Literal(s) = &info.token {
!s.is_empty() && s.chars().all(|c| c.is_whitespace())
} else {
false
}
}
fn render_with_layout(
ctx: &RowContext,
left: &[TokenInfo],
right: &[TokenInfo],
width: usize,
mut available: usize,
) -> Vec<Span<'static>> {
let mut spans = Vec::new();
let mut used_width = 0;
let mut first_flex_assigned = false;
let mut slack: usize = 0;
let mut user_style = Style::default();
for info in left {
match &info.token {
Token::Literal(s) => {
spans.push(styled_span(s.clone(), Style::default(), user_style, ctx));
used_width += info.natural_width;
}
Token::Fill => {}
Token::Style(directive) => {
user_style = apply_tmux_directives(user_style, directive, Style::default());
}
Token::Field(id) => {
if info.is_flex && !first_flex_assigned {
let allocated = available;
if *id == TokenId::StatusIcon {
for (text, style) in &ctx.status_icon_spans {
spans.push(styled_span(text.clone(), *style, user_style, ctx));
used_width += display_width(text);
}
} else {
let text = ctx.resolve(*id);
let rendered = if info.natural_width > allocated && allocated > 0 {
truncate_with_ellipsis(&text, allocated)
} else if allocated == 0 {
String::new()
} else {
text
};
let rendered_width = display_width(&rendered);
spans.push(styled_span(
rendered,
ctx.intrinsic_style(*id),
user_style,
ctx,
));
used_width += rendered_width;
if rendered_width < allocated {
slack = allocated - rendered_width;
}
}
first_flex_assigned = true;
available = 0;
} else {
let max_w = width.saturating_sub(used_width);
if *id == TokenId::StatusIcon {
for (text, style) in &ctx.status_icon_spans {
spans.push(styled_span(text.clone(), *style, user_style, ctx));
used_width += display_width(text);
}
} else if is_git_segment(*id) {
let (git_spans, git_width) = ctx.git_segment_spans(*id, max_w);
for (text, style) in git_spans {
spans.push(styled_span(text, style, user_style, ctx));
}
used_width += git_width;
} else {
let text = ctx.resolve(*id);
let rendered = if info.natural_width > max_w && max_w > 0 {
truncate_with_ellipsis(&text, max_w)
} else {
text
};
spans.push(styled_span(
rendered,
ctx.intrinsic_style(*id),
user_style,
ctx,
));
used_width += info.natural_width.min(max_w);
}
}
}
}
}
let fill_width = if first_flex_assigned {
slack
} else {
available
};
if fill_width > 0 {
spans.push(styled_span(
" ".repeat(fill_width),
Style::default(),
user_style,
ctx,
));
used_width += fill_width;
}
for info in right {
match &info.token {
Token::Literal(s) => {
spans.push(styled_span(s.clone(), Style::default(), user_style, ctx));
used_width += info.natural_width;
}
Token::Fill => {}
Token::Style(directive) => {
user_style = apply_tmux_directives(user_style, directive, Style::default());
}
Token::Field(id) => {
let max_w = width.saturating_sub(used_width);
if *id == TokenId::StatusIcon {
for (text, style) in &ctx.status_icon_spans {
spans.push(styled_span(text.clone(), *style, user_style, ctx));
used_width += display_width(text);
}
} else if is_git_segment(*id) {
let (git_spans, git_width) = ctx.git_segment_spans(*id, max_w);
for (text, style) in git_spans {
spans.push(styled_span(text, style, user_style, ctx));
}
used_width += git_width;
} else {
let text = ctx.resolve(*id);
let rendered = if info.natural_width > max_w && max_w > 0 {
truncate_with_ellipsis(&text, max_w)
} else {
text
};
spans.push(styled_span(
rendered,
ctx.intrinsic_style(*id),
user_style,
ctx,
));
used_width += info.natural_width.min(max_w);
}
}
}
}
if used_width < width {
spans.push(styled_span(
" ".repeat(width - used_width),
Style::default(),
user_style,
ctx,
));
}
spans
}
fn styled_span(text: String, base: Style, user_style: Style, ctx: &RowContext) -> Span<'static> {
if ctx.is_stale {
Span::styled(text, base)
} else {
Span::styled(text, base.patch(user_style))
}
}
fn display_width(s: &str) -> usize {
s.chars()
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(1))
.sum()
}
fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
if max_width == 0 {
return String::new();
}
if display_width(s) <= max_width {
return s.to_string();
}
if max_width == 1 {
return "\u{2026}".to_string();
}
let mut out = String::new();
let mut width = 0;
for c in s.chars() {
let char_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(1);
if width + char_width + 1 > max_width {
break;
}
out.push(c);
width += char_width;
}
let trimmed = out.trim_end();
let mut result = trimmed.to_string();
result.push('\u{2026}');
result
}
#[cfg(test)]
mod tests {
use super::super::context::RowContext;
use super::super::parser::{Token, TokenId};
use super::*;
use crate::git::GitStatus;
use crate::multiplexer::AgentPane;
use crate::ui::theme::ThemePalette;
use std::path::PathBuf;
fn test_palette() -> &'static ThemePalette {
use crate::config::{ThemeMode, ThemeScheme};
Box::leak(Box::new(ThemePalette::for_scheme(
ThemeScheme::Default,
ThemeMode::Dark,
)))
}
fn test_agent(name: &str) -> AgentPane {
AgentPane {
session: "session".to_string(),
window_name: format!("wm-{}", name),
pane_id: "%1".to_string(),
window_id: "@1".to_string(),
path: PathBuf::from(format!("/tmp/{}", name)),
pane_title: None,
status: None,
status_ts: None,
updated_ts: None,
window_cmd: None,
agent_command: None,
agent_kind: None,
}
}
fn make_context(agent: &AgentPane) -> RowContext<'_> {
RowContext {
agent,
primary: "feature-auth".to_string(),
secondary: "myproject".to_string(),
pane_suffix: String::new(),
elapsed: "5:23".to_string(),
status_icon_spans: vec![("💤".to_string(), ratatui::style::Style::default())],
status_color: ratatui::style::Color::Reset,
pane_title: None,
git_status: None,
is_stale: false,
is_active: false,
is_selected: false,
palette: test_palette(),
agent_icon: String::new(),
agent_icon_color: None,
agent_label: String::new(),
idx: 0,
}
}
#[test]
fn render_line_with_fill() {
let agent = test_agent("foo");
let ctx = make_context(&agent);
let tokens = vec![
Token::Field(TokenId::Primary),
Token::Literal(" ".to_string()),
Token::Fill,
Token::Literal(" ".to_string()),
Token::Field(TokenId::Elapsed),
];
let spans = render_line(&ctx, &tokens, 20);
let text: String = spans.iter().map(|s| s.content.clone()).collect();
assert!(text.contains("feature-auth"));
assert!(text.contains("5:23"));
}
#[test]
fn render_line_narrow_truncates_flex() {
let agent = test_agent("foo");
let ctx = make_context(&agent);
let tokens = vec![
Token::Field(TokenId::Primary),
Token::Fill,
Token::Field(TokenId::Elapsed),
];
let spans = render_line(&ctx, &tokens, 10);
let text: String = spans.iter().map(|s| s.content.clone()).collect();
assert!(text.contains("5:23"));
assert!(text.contains('…'));
}
#[test]
fn render_line_drops_right_token_when_narrow() {
let agent = test_agent("foo");
let ctx = make_context(&agent);
let tokens = vec![
Token::Field(TokenId::Primary),
Token::Literal(" ".to_string()),
Token::Fill,
Token::Literal(" ".to_string()),
Token::Field(TokenId::Elapsed),
];
let spans = render_line(&ctx, &tokens, 4);
let text: String = spans.iter().map(|s| s.content.clone()).collect();
assert!(!text.contains("5:23"));
assert!(text.contains('…'));
}
#[test]
fn empty_line_when_all_variable_tokens_empty() {
let agent = test_agent("foo");
let ctx = RowContext {
agent: &agent,
primary: String::new(),
secondary: String::new(),
pane_suffix: String::new(),
elapsed: String::new(),
status_icon_spans: vec![],
status_color: ratatui::style::Color::Reset,
pane_title: None,
git_status: None,
is_stale: false,
is_active: false,
is_selected: false,
palette: test_palette(),
agent_icon: String::new(),
agent_icon_color: None,
agent_label: String::new(),
idx: 0,
};
let tokens = vec![
Token::Field(TokenId::PaneTitle),
Token::Literal(" ".to_string()),
Token::Fill,
];
assert!(is_empty_line(&tokens, &ctx));
}
#[test]
fn non_empty_line_with_literal_content() {
let agent = test_agent("foo");
let ctx = make_context(&agent);
let tokens = vec![
Token::Literal("▌ ".to_string()),
Token::Field(TokenId::Primary),
];
assert!(!is_empty_line(&tokens, &ctx));
}
fn make_git_context<'a>(agent: &'a AgentPane, status: &'a GitStatus) -> RowContext<'a> {
RowContext {
agent,
primary: String::new(),
secondary: String::new(),
pane_suffix: String::new(),
elapsed: String::new(),
status_icon_spans: vec![],
status_color: ratatui::style::Color::Reset,
pane_title: None,
git_status: Some(status),
is_stale: false,
is_active: false,
is_selected: false,
palette: test_palette(),
agent_icon: String::new(),
agent_icon_color: None,
agent_label: String::new(),
idx: 0,
}
}
fn render_text(ctx: &RowContext, tokens: &[Token], width: usize) -> String {
render_line(ctx, tokens, width)
.iter()
.map(|s| s.content.clone())
.collect()
}
#[test]
fn split_git_tokens_join_with_literal_space() {
let agent = test_agent("g");
let status = GitStatus {
lines_added: 10,
lines_removed: 5,
uncommitted_added: 3,
uncommitted_removed: 1,
is_dirty: true,
..Default::default()
};
let ctx = make_git_context(&agent, &status);
let tokens = vec![
Token::Field(TokenId::GitCommitted),
Token::Literal(" ".to_string()),
Token::Field(TokenId::GitUncommitted),
];
let text = render_text(&ctx, &tokens, 40);
assert!(text.contains("+10 -5"), "missing committed: {:?}", text);
assert!(text.contains("+3 -1"), "missing uncommitted: {:?}", text);
}
#[test]
fn split_git_tokens_collapse_space_when_one_empty() {
let agent = test_agent("g");
let status = GitStatus {
uncommitted_added: 3,
uncommitted_removed: 1,
is_dirty: true,
..Default::default()
};
let ctx = make_git_context(&agent, &status);
let tokens = vec![
Token::Field(TokenId::GitCommitted),
Token::Literal(" ".to_string()),
Token::Field(TokenId::GitUncommitted),
];
let text = render_text(&ctx, &tokens, 40);
assert!(
!text.starts_with(" "),
"leading whitespace bleed: {:?}",
text
);
assert!(text.contains("+3 -1"));
}
#[test]
fn split_git_committed_self_fits_when_narrow() {
let agent = test_agent("g");
let status = GitStatus {
lines_added: 1278,
lines_removed: 400,
..Default::default()
};
let ctx = make_git_context(&agent, &status);
let tokens = vec![Token::Field(TokenId::GitCommitted)];
let text: String = render_line(&ctx, &tokens, 7)
.iter()
.map(|s| s.content.clone())
.collect();
assert!(text.contains("+1278"), "missing +1278: {:?}", text);
assert!(!text.contains("-400"), "should drop -400: {:?}", text);
}
#[test]
fn split_git_uncommitted_falls_back_to_icon_only() {
let agent = test_agent("g");
let status = GitStatus {
uncommitted_added: 999,
uncommitted_removed: 999,
is_dirty: true,
..Default::default()
};
let ctx = make_git_context(&agent, &status);
let tokens = vec![Token::Field(TokenId::GitUncommitted)];
let spans = render_line(&ctx, &tokens, 1);
let text: String = spans.iter().map(|s| s.content.clone()).collect();
assert!(!text.contains('+'), "should drop numbers: {:?}", text);
assert!(!text.contains('-'), "should drop numbers: {:?}", text);
}
#[test]
fn split_git_token_renders_on_right_side_of_fill() {
let agent = test_agent("g");
let status = GitStatus {
lines_added: 10,
lines_removed: 5,
..Default::default()
};
let ctx = make_git_context(&agent, &status);
let tokens = vec![Token::Fill, Token::Field(TokenId::GitCommitted)];
let text = render_text(&ctx, &tokens, 40);
assert!(text.contains("+10 -5"), "missing committed: {:?}", text);
}
#[test]
fn is_empty_line_with_only_git_committed_empty() {
let agent = test_agent("g");
let status = GitStatus::default();
let ctx = make_git_context(&agent, &status);
let tokens = vec![Token::Field(TokenId::GitCommitted)];
assert!(is_empty_line(&tokens, &ctx));
}
#[test]
fn is_empty_line_false_when_git_committed_has_content() {
let agent = test_agent("g");
let status = GitStatus {
lines_added: 5,
..Default::default()
};
let ctx = make_git_context(&agent, &status);
let tokens = vec![Token::Field(TokenId::GitCommitted)];
assert!(!is_empty_line(&tokens, &ctx));
}
#[test]
fn trailing_style_after_right_field_does_not_change_drop_behavior() {
let agent = test_agent("foo");
let ctx = make_context(&agent);
let plain = vec![
Token::Field(TokenId::Primary),
Token::Fill,
Token::Field(TokenId::Elapsed),
];
let styled = vec![
Token::Field(TokenId::Primary),
Token::Fill,
Token::Field(TokenId::Elapsed),
Token::Style("default".to_string()),
];
let plain_text = render_text(&ctx, &plain, 3);
let styled_text = render_text(&ctx, &styled, 3);
assert_eq!(plain_text, styled_text);
}
#[test]
fn style_token_is_zero_width_in_layout() {
let agent = test_agent("foo");
let ctx = make_context(&agent);
let plain = vec![
Token::Field(TokenId::Primary),
Token::Fill,
Token::Field(TokenId::Elapsed),
];
let styled = vec![
Token::Style("fg=red".to_string()),
Token::Field(TokenId::Primary),
Token::Style("default".to_string()),
Token::Fill,
Token::Field(TokenId::Elapsed),
];
let plain_text: String = render_line(&ctx, &plain, 30)
.iter()
.map(|s| s.content.clone())
.collect();
let styled_text: String = render_line(&ctx, &styled, 30)
.iter()
.map(|s| s.content.clone())
.collect();
assert_eq!(plain_text, styled_text);
}
#[test]
fn user_fg_overrides_intrinsic_on_field() {
use ratatui::style::Color;
let agent = test_agent("foo");
let ctx = make_context(&agent);
let tokens = vec![
Token::Style("fg=red".to_string()),
Token::Field(TokenId::Primary),
Token::Style("default".to_string()),
];
let spans = render_line(&ctx, &tokens, 20);
let primary_span = spans
.iter()
.find(|s| s.content.contains("feature-auth"))
.expect("primary rendered");
assert_eq!(primary_span.style.fg, Some(Color::Red));
}
#[test]
fn default_resets_user_overlay() {
use ratatui::style::Color;
let agent = test_agent("foo");
let ctx = make_context(&agent);
let tokens = vec![
Token::Style("fg=red".to_string()),
Token::Literal("x".to_string()),
Token::Style("default".to_string()),
Token::Literal("y".to_string()),
];
let spans = render_line(&ctx, &tokens, 10);
let x = spans.iter().find(|s| s.content == "x").unwrap();
let y = spans.iter().find(|s| s.content == "y").unwrap();
assert_eq!(x.style.fg, Some(Color::Red));
assert_eq!(y.style.fg, None);
}
#[test]
fn stale_row_ignores_user_overlay() {
let agent = test_agent("foo");
let mut ctx = make_context(&agent);
ctx.is_stale = true;
let plain_tokens = vec![Token::Field(TokenId::Primary)];
let styled_tokens = vec![
Token::Style("fg=red,bold".to_string()),
Token::Field(TokenId::Primary),
];
let plain = render_line(&ctx, &plain_tokens, 20);
let styled = render_line(&ctx, &styled_tokens, 20);
let plain_primary = plain
.iter()
.find(|s| s.content.contains("feature-auth"))
.unwrap();
let styled_primary = styled
.iter()
.find(|s| s.content.contains("feature-auth"))
.unwrap();
assert_eq!(plain_primary.style, styled_primary.style);
}
#[test]
fn unclosed_style_renders_literally_and_counts_for_width() {
let agent = test_agent("foo");
let ctx = make_context(&agent);
use super::super::parser::parse_line;
let tokens = parse_line("#[fg=red x").unwrap();
let spans = render_line(&ctx, &tokens, 20);
let text: String = spans.iter().map(|s| s.content.clone()).collect();
assert!(text.contains("#[fg=red x"));
}
#[test]
fn rendered_width_matches_requested_width_with_styles() {
let agent = test_agent("foo");
let ctx = make_context(&agent);
let tokens = vec![
Token::Style("fg=red".to_string()),
Token::Field(TokenId::Primary),
Token::Style("default".to_string()),
Token::Fill,
Token::Field(TokenId::Elapsed),
];
let spans = render_line(&ctx, &tokens, 25);
let total_width: usize = spans.iter().map(|s| display_width(&s.content)).sum();
assert_eq!(total_width, 25);
}
#[test]
fn styled_fill_padding_inherits_overlay() {
use ratatui::style::Color;
let agent = test_agent("foo");
let ctx = make_context(&agent);
let tokens = vec![
Token::Style("bg=blue".to_string()),
Token::Field(TokenId::Primary),
Token::Fill,
Token::Field(TokenId::Elapsed),
];
let spans = render_line(&ctx, &tokens, 30);
let pad = spans
.iter()
.find(|s| !s.content.is_empty() && s.content.chars().all(|c| c == ' '))
.expect("a padding span exists");
assert_eq!(pad.style.bg, Some(Color::Blue));
}
#[test]
fn collapse_empty_field_skips_intervening_style_token() {
let agent = test_agent("foo");
let mut ctx = make_context(&agent);
ctx.pane_suffix.clear();
let tokens = vec![
Token::Field(TokenId::Primary),
Token::Field(TokenId::PaneSuffix),
Token::Style("fg=red".to_string()),
Token::Literal(" ".to_string()),
Token::Field(TokenId::Elapsed),
];
let spans = render_line(&ctx, &tokens, 30);
let text: String = spans.iter().map(|s| s.content.clone()).collect();
assert!(
!text.contains("feature-auth "),
"joiner space leaked: {:?}",
text
);
}
#[test]
fn is_empty_line_ignores_style_tokens() {
let agent = test_agent("foo");
let ctx = RowContext {
agent: &agent,
primary: String::new(),
secondary: String::new(),
pane_suffix: String::new(),
elapsed: String::new(),
status_icon_spans: vec![],
status_color: ratatui::style::Color::Reset,
pane_title: None,
git_status: None,
is_stale: false,
is_active: false,
is_selected: false,
palette: test_palette(),
agent_icon: String::new(),
agent_icon_color: None,
agent_label: String::new(),
idx: 0,
};
let tokens = vec![
Token::Style("fg=red".to_string()),
Token::Field(TokenId::PaneTitle),
Token::Style("default".to_string()),
];
assert!(is_empty_line(&tokens, &ctx));
}
#[test]
fn empty_line_with_only_whitespace_literal() {
let agent = test_agent("foo");
let ctx = RowContext {
agent: &agent,
primary: String::new(),
secondary: String::new(),
pane_suffix: String::new(),
elapsed: String::new(),
status_icon_spans: vec![],
status_color: ratatui::style::Color::Reset,
pane_title: None,
git_status: None,
is_stale: false,
is_active: false,
is_selected: false,
palette: test_palette(),
agent_icon: String::new(),
agent_icon_color: None,
agent_label: String::new(),
idx: 0,
};
let tokens = vec![
Token::Literal(" ".to_string()),
Token::Field(TokenId::PaneTitle),
];
assert!(is_empty_line(&tokens, &ctx));
}
}