feat: complete Rust port of claude-statusline
Port the entire 2236-line bash statusline script to Rust. Implements all 25 sections, 3-phase layout engine (render, priority drop, flex/justify), file-based caching with flock, 9-level terminal width detection, trend sparklines, and deep-merge JSON config. Release binary: 864K with LTO. Render time: <1ms warm. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
177
src/layout/mod.rs
Normal file
177
src/layout/mod.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
pub mod flex;
|
||||
pub mod justify;
|
||||
pub mod priority;
|
||||
|
||||
use crate::config::{Config, JustifyMode, LayoutValue};
|
||||
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(output) = section::render_section(id, ctx) {
|
||||
if output.raw.is_empty() && !section::is_spacer(id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user