Files
claude-statusline/src/layout/mod.rs
Taylor Eernisse 9c24617642 fix: 4 bugs found during code review against PRD
- output_style: use MAGENTA color per PRD (was incorrectly DIM)
- vcs: pass project dir to jj exec (was None, causing wrong cwd)
- tools: singular "tool" when count is 1 (was always "tools")
- layout: wire up apply_formatting() in render pipeline (prefix,
  suffix, color override, pad, align were completely dead code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 14:28:50 -05:00

222 lines
7.9 KiB
Rust

pub mod flex;
pub mod justify;
pub mod priority;
use crate::config::{Config, JustifyMode, LayoutValue, SectionBase};
use crate::format;
use crate::section::{self, RenderContext, SectionOutput};
/// A section that survived priority drops and has rendered output.
pub struct ActiveSection {
pub id: String,
pub output: SectionOutput,
pub priority: u8,
pub is_spacer: bool,
pub is_flex: bool,
}
/// Resolve layout: preset lookup with optional responsive override.
pub fn resolve_layout(config: &Config, term_width: u16) -> Vec<Vec<String>> {
match &config.layout {
LayoutValue::Preset(name) => {
let effective = if config.global.responsive {
responsive_preset(term_width, &config.global.breakpoints)
} else {
name.as_str()
};
config
.presets
.get(effective)
.or_else(|| config.presets.get(name.as_str()))
.cloned()
.unwrap_or_else(|| vec![vec!["model".into(), "project".into()]])
}
LayoutValue::Custom(lines) => lines.clone(),
}
}
fn responsive_preset(width: u16, bp: &crate::config::Breakpoints) -> &'static str {
if width < bp.narrow {
"dense"
} else if width < bp.medium {
"standard"
} else {
"verbose"
}
}
/// Full render: resolve layout, render each line, join with newlines.
pub fn render_all(ctx: &RenderContext) -> String {
let layout = resolve_layout(ctx.config, ctx.term_width);
let separator = &ctx.config.global.separator;
let lines: Vec<String> = layout
.iter()
.filter_map(|line_ids| render_line(line_ids, ctx, separator))
.collect();
lines.join("\n")
}
/// Render a single layout line.
/// Three phases: render all -> priority drop -> flex/justify.
fn render_line(section_ids: &[String], ctx: &RenderContext, separator: &str) -> Option<String> {
// Phase 1: Render all sections, collect active ones
let mut active: Vec<ActiveSection> = Vec::new();
for id in section_ids {
if let Some(mut output) = section::render_section(id, ctx) {
if output.raw.is_empty() && !section::is_spacer(id) {
continue;
}
// Apply per-section formatting (prefix, suffix, color override, pad, align)
if let Some(base) = section_base(id, ctx.config) {
format::apply_formatting(
&mut output.raw,
&mut output.ansi,
base,
ctx.theme,
&ctx.config.colors,
);
}
let (prio, is_flex) = section_meta(id, ctx.config);
active.push(ActiveSection {
id: id.clone(),
output,
priority: prio,
is_spacer: section::is_spacer(id),
is_flex,
});
}
}
if active.is_empty() || active.iter().all(|s| s.is_spacer) {
return None;
}
// Phase 2: Priority drop if overflowing
let mut active = priority::priority_drop(active, ctx.term_width, separator);
// Phase 3: Flex expand or justify
let line = if ctx.config.global.justify != JustifyMode::Left
&& !active.iter().any(|s| s.is_spacer)
&& active.len() > 1
{
justify::justify(
&active,
ctx.term_width,
separator,
ctx.config.global.justify,
)
} else {
flex::flex_expand(&mut active, ctx, separator);
assemble_left(&active, separator, ctx.color_enabled)
};
Some(line)
}
/// Left-aligned assembly with separator dimming and spacer suppression.
fn assemble_left(active: &[ActiveSection], separator: &str, color_enabled: bool) -> String {
let mut output = String::new();
let mut prev_is_spacer = false;
for (i, sec) in active.iter().enumerate() {
if i > 0 && !prev_is_spacer && !sec.is_spacer {
if color_enabled {
output.push_str(&format!(
"{}{separator}{}",
crate::color::DIM,
crate::color::RESET
));
} else {
output.push_str(separator);
}
}
output.push_str(&sec.output.ansi);
prev_is_spacer = sec.is_spacer;
}
output
}
/// Look up section priority and flex from config.
fn section_meta(id: &str, config: &Config) -> (u8, bool) {
if section::is_spacer(id) {
return (1, true);
}
macro_rules! meta_base {
($section:expr) => {
($section.priority, $section.flex)
};
}
macro_rules! meta_flat {
($section:expr) => {
($section.base.priority, $section.base.flex)
};
}
match id {
"model" => meta_base!(config.sections.model),
"provider" => meta_base!(config.sections.provider),
"project" => meta_flat!(config.sections.project),
"vcs" => meta_flat!(config.sections.vcs),
"beads" => meta_flat!(config.sections.beads),
"context_bar" => meta_flat!(config.sections.context_bar),
"context_usage" => meta_flat!(config.sections.context_usage),
"context_remaining" => meta_flat!(config.sections.context_remaining),
"tokens_raw" => meta_flat!(config.sections.tokens_raw),
"cache_efficiency" => meta_base!(config.sections.cache_efficiency),
"cost" => meta_flat!(config.sections.cost),
"cost_velocity" => meta_base!(config.sections.cost_velocity),
"token_velocity" => meta_base!(config.sections.token_velocity),
"cost_trend" => meta_flat!(config.sections.cost_trend),
"context_trend" => meta_flat!(config.sections.context_trend),
"lines_changed" => meta_base!(config.sections.lines_changed),
"duration" => meta_base!(config.sections.duration),
"tools" => meta_flat!(config.sections.tools),
"turns" => meta_flat!(config.sections.turns),
"load" => meta_flat!(config.sections.load),
"version" => meta_base!(config.sections.version),
"time" => meta_flat!(config.sections.time),
"output_style" => meta_base!(config.sections.output_style),
"hostname" => meta_base!(config.sections.hostname),
_ => (2, false), // custom sections default
}
}
/// Look up the SectionBase for a given section ID.
/// Returns None for spacers and unknown custom sections.
fn section_base<'a>(id: &str, config: &'a Config) -> Option<&'a SectionBase> {
match id {
"model" => Some(&config.sections.model),
"provider" => Some(&config.sections.provider),
"project" => Some(&config.sections.project.base),
"vcs" => Some(&config.sections.vcs.base),
"beads" => Some(&config.sections.beads.base),
"context_bar" => Some(&config.sections.context_bar.base),
"context_usage" => Some(&config.sections.context_usage.base),
"context_remaining" => Some(&config.sections.context_remaining.base),
"tokens_raw" => Some(&config.sections.tokens_raw.base),
"cache_efficiency" => Some(&config.sections.cache_efficiency),
"cost" => Some(&config.sections.cost.base),
"cost_velocity" => Some(&config.sections.cost_velocity),
"token_velocity" => Some(&config.sections.token_velocity),
"cost_trend" => Some(&config.sections.cost_trend.base),
"context_trend" => Some(&config.sections.context_trend.base),
"lines_changed" => Some(&config.sections.lines_changed),
"duration" => Some(&config.sections.duration),
"tools" => Some(&config.sections.tools.base),
"turns" => Some(&config.sections.turns.base),
"load" => Some(&config.sections.load.base),
"version" => Some(&config.sections.version),
"time" => Some(&config.sections.time.base),
"output_style" => Some(&config.sections.output_style),
"hostname" => Some(&config.sections.hostname),
_ => None,
}
}