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> { 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 = 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 { // Phase 1: Render all sections, collect active ones let mut active: Vec = 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, } }