- 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>
222 lines
7.9 KiB
Rust
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,
|
|
}
|
|
}
|