fix: width detection prioritizes dynamic sources over config
Config `width` was position #3 in the detection chain, overriding all dynamic detection (ioctl, process tree, stty, etc). This meant the statusline couldn't adapt to terminal/pane resizes. Now config `width` serves two roles: - Max cap on dynamically detected widths (prevents absurd widths) - Fallback when all dynamic detection methods fail Also adds: - ioctl on stderr (works when stdout is piped) - stdin JSON `terminal_width` field for Claude Code to pass width - Distinct diagnostic sources: config_cap, config_fallback, stdin_json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -122,8 +122,14 @@ fn main() {
|
||||
|
||||
// Detect environment
|
||||
let detected_theme = theme::detect_theme(&config);
|
||||
let (term_width, width_source) =
|
||||
width::detect_width_with_source(cli_width, config.global.width, config.global.width_margin);
|
||||
let stdin_width = input_data.workspace.as_ref().and_then(|w| w.terminal_width);
|
||||
let (term_width, width_source) = width::detect_width_with_source(
|
||||
cli_width,
|
||||
config.global.width,
|
||||
config.global.width_margin,
|
||||
stdin_width,
|
||||
);
|
||||
|
||||
let tier = width::width_tier(
|
||||
term_width,
|
||||
config.global.breakpoints.narrow,
|
||||
|
||||
@@ -51,6 +51,7 @@ pub struct CurrentUsage {
|
||||
#[serde(default)]
|
||||
pub struct Workspace {
|
||||
pub project_dir: Option<String>,
|
||||
pub terminal_width: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
|
||||
95
src/width.rs
95
src/width.rs
@@ -22,9 +22,24 @@ pub fn width_tier(width: u16, narrow_bp: u16, medium_bp: u16) -> WidthTier {
|
||||
}
|
||||
|
||||
/// Detect terminal width. Memoized for 1 second across renders.
|
||||
/// Priority: cli_width > env > config > ioctl > process tree > stty > COLUMNS > tput > 120
|
||||
pub fn detect_width(cli_width: Option<u16>, config_width: Option<u16>, config_margin: u16) -> u16 {
|
||||
detect_width_with_source(cli_width, config_width, config_margin).0
|
||||
///
|
||||
/// Priority chain:
|
||||
/// 1. `--width` CLI flag (absolute override)
|
||||
/// 2. `CLAUDE_STATUSLINE_WIDTH` env var (absolute override)
|
||||
/// 3. stdin JSON `terminal_width` (from Claude Code)
|
||||
/// 4. ioctl stdout, 5. ioctl stderr, 6. process tree, 7. stty, 8. COLUMNS, 9. tput
|
||||
/// 10. Config `width` (fallback only)
|
||||
/// 11. Hardcoded 120
|
||||
///
|
||||
/// Config `width` also acts as a max cap on dynamic results (items 3-9).
|
||||
/// CLI flag and env var are NOT capped — they're explicit user overrides.
|
||||
pub fn detect_width(
|
||||
cli_width: Option<u16>,
|
||||
config_width: Option<u16>,
|
||||
config_margin: u16,
|
||||
stdin_width: Option<u16>,
|
||||
) -> u16 {
|
||||
detect_width_with_source(cli_width, config_width, config_margin, stdin_width).0
|
||||
}
|
||||
|
||||
/// Like detect_width but also returns the detection source name (for --dump-state).
|
||||
@@ -32,6 +47,7 @@ pub fn detect_width_with_source(
|
||||
cli_width: Option<u16>,
|
||||
config_width: Option<u16>,
|
||||
config_margin: u16,
|
||||
stdin_width: Option<u16>,
|
||||
) -> (u16, &'static str) {
|
||||
// Check memo first
|
||||
if let Ok(guard) = CACHED_WIDTH.lock() {
|
||||
@@ -42,7 +58,7 @@ pub fn detect_width_with_source(
|
||||
}
|
||||
}
|
||||
|
||||
let (raw, source) = detect_raw_with_source(cli_width, config_width);
|
||||
let (raw, source) = detect_raw_with_source(cli_width, config_width, stdin_width);
|
||||
let effective = raw.saturating_sub(config_margin).max(40);
|
||||
|
||||
// Store in memo
|
||||
@@ -56,15 +72,16 @@ pub fn detect_width_with_source(
|
||||
fn detect_raw_with_source(
|
||||
cli_width: Option<u16>,
|
||||
config_width: Option<u16>,
|
||||
stdin_width: Option<u16>,
|
||||
) -> (u16, &'static str) {
|
||||
// 1. --width CLI flag
|
||||
// 1. --width CLI flag (absolute, not capped)
|
||||
if let Some(w) = cli_width {
|
||||
if w > 0 {
|
||||
return (w, "cli_flag");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. CLAUDE_STATUSLINE_WIDTH env var
|
||||
// 2. CLAUDE_STATUSLINE_WIDTH env var (absolute, not capped)
|
||||
if let Ok(val) = std::env::var("CLAUDE_STATUSLINE_WIDTH") {
|
||||
if let Ok(w) = val.parse::<u16>() {
|
||||
if w > 0 {
|
||||
@@ -73,52 +90,92 @@ fn detect_raw_with_source(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Config override
|
||||
// 3-9: Dynamic detection, subject to config max cap
|
||||
if let Some((w, source)) = detect_dynamic(stdin_width) {
|
||||
return apply_config_cap(w, source, config_width);
|
||||
}
|
||||
|
||||
// 10. Config width as fallback
|
||||
if let Some(w) = config_width {
|
||||
if w > 0 {
|
||||
return (w, "config");
|
||||
return (w, "config_fallback");
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Hardcoded fallback
|
||||
(120, "fallback")
|
||||
}
|
||||
|
||||
/// Try all dynamic detection methods in priority order.
|
||||
/// Returns the first successful result.
|
||||
fn detect_dynamic(stdin_width: Option<u16>) -> Option<(u16, &'static str)> {
|
||||
// 3. stdin JSON terminal_width (Claude Code passes this)
|
||||
if let Some(w) = stdin_width {
|
||||
if w > 0 {
|
||||
return Some((w, "stdin_json"));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. ioctl(TIOCGWINSZ) on stdout
|
||||
if let Some(w) = ioctl_width(libc::STDOUT_FILENO) {
|
||||
if w > 0 {
|
||||
return (w, "ioctl");
|
||||
return Some((w, "ioctl_stdout"));
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Process tree walk: find ancestor with real TTY
|
||||
// 5. ioctl(TIOCGWINSZ) on stderr (often still a TTY when stdout is piped)
|
||||
if let Some(w) = ioctl_width(libc::STDERR_FILENO) {
|
||||
if w > 0 {
|
||||
return Some((w, "ioctl_stderr"));
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Process tree walk: find ancestor with real TTY
|
||||
if let Some(w) = process_tree_width() {
|
||||
if w > 0 {
|
||||
return (w, "process_tree");
|
||||
return Some((w, "process_tree"));
|
||||
}
|
||||
}
|
||||
|
||||
// 6. stty size < /dev/tty
|
||||
// 7. stty size < /dev/tty
|
||||
if let Some(w) = stty_dev_tty() {
|
||||
if w > 0 {
|
||||
return (w, "stty");
|
||||
return Some((w, "stty"));
|
||||
}
|
||||
}
|
||||
|
||||
// 7. COLUMNS env var
|
||||
// 8. COLUMNS env var
|
||||
if let Ok(val) = std::env::var("COLUMNS") {
|
||||
if let Ok(w) = val.parse::<u16>() {
|
||||
if w > 0 {
|
||||
return (w, "columns_env");
|
||||
return Some((w, "columns_env"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. tput cols
|
||||
// 9. tput cols
|
||||
if let Some(w) = tput_cols() {
|
||||
if w > 0 {
|
||||
return (w, "tput");
|
||||
return Some((w, "tput"));
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Fallback
|
||||
(120, "fallback")
|
||||
None
|
||||
}
|
||||
|
||||
/// Apply config width as a max cap on dynamically detected width.
|
||||
/// If the detected width exceeds config, clamp it down and report the cap.
|
||||
fn apply_config_cap(
|
||||
detected: u16,
|
||||
source: &'static str,
|
||||
config_width: Option<u16>,
|
||||
) -> (u16, &'static str) {
|
||||
if let Some(max_w) = config_width {
|
||||
if max_w > 0 && detected > max_w {
|
||||
return (max_w, "config_cap");
|
||||
}
|
||||
}
|
||||
(detected, source)
|
||||
}
|
||||
|
||||
fn ioctl_width(fd: i32) -> Option<u16> {
|
||||
|
||||
28
statusline.json
Normal file
28
statusline.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "./schema.json",
|
||||
"global": {
|
||||
"justify": "spread",
|
||||
"theme": "dark",
|
||||
"responsive": true,
|
||||
"width": 500,
|
||||
"width_margin": 20
|
||||
},
|
||||
"glyphs": {
|
||||
"enabled": true
|
||||
},
|
||||
"layout": "verbose",
|
||||
"sections": {
|
||||
"context_usage": {
|
||||
"enabled": true
|
||||
},
|
||||
"context_trend": {
|
||||
"enabled": true
|
||||
},
|
||||
"cost_trend": {
|
||||
"enabled": true
|
||||
},
|
||||
"load": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user