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
|
// Detect environment
|
||||||
let detected_theme = theme::detect_theme(&config);
|
let detected_theme = theme::detect_theme(&config);
|
||||||
let (term_width, width_source) =
|
let stdin_width = input_data.workspace.as_ref().and_then(|w| w.terminal_width);
|
||||||
width::detect_width_with_source(cli_width, config.global.width, config.global.width_margin);
|
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(
|
let tier = width::width_tier(
|
||||||
term_width,
|
term_width,
|
||||||
config.global.breakpoints.narrow,
|
config.global.breakpoints.narrow,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ pub struct CurrentUsage {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
pub project_dir: Option<String>,
|
pub project_dir: Option<String>,
|
||||||
|
pub terminal_width: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[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.
|
/// 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 {
|
/// Priority chain:
|
||||||
detect_width_with_source(cli_width, config_width, config_margin).0
|
/// 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).
|
/// 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>,
|
cli_width: Option<u16>,
|
||||||
config_width: Option<u16>,
|
config_width: Option<u16>,
|
||||||
config_margin: u16,
|
config_margin: u16,
|
||||||
|
stdin_width: Option<u16>,
|
||||||
) -> (u16, &'static str) {
|
) -> (u16, &'static str) {
|
||||||
// Check memo first
|
// Check memo first
|
||||||
if let Ok(guard) = CACHED_WIDTH.lock() {
|
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);
|
let effective = raw.saturating_sub(config_margin).max(40);
|
||||||
|
|
||||||
// Store in memo
|
// Store in memo
|
||||||
@@ -56,15 +72,16 @@ pub fn detect_width_with_source(
|
|||||||
fn detect_raw_with_source(
|
fn detect_raw_with_source(
|
||||||
cli_width: Option<u16>,
|
cli_width: Option<u16>,
|
||||||
config_width: Option<u16>,
|
config_width: Option<u16>,
|
||||||
|
stdin_width: Option<u16>,
|
||||||
) -> (u16, &'static str) {
|
) -> (u16, &'static str) {
|
||||||
// 1. --width CLI flag
|
// 1. --width CLI flag (absolute, not capped)
|
||||||
if let Some(w) = cli_width {
|
if let Some(w) = cli_width {
|
||||||
if w > 0 {
|
if w > 0 {
|
||||||
return (w, "cli_flag");
|
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(val) = std::env::var("CLAUDE_STATUSLINE_WIDTH") {
|
||||||
if let Ok(w) = val.parse::<u16>() {
|
if let Ok(w) = val.parse::<u16>() {
|
||||||
if w > 0 {
|
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 let Some(w) = config_width {
|
||||||
if w > 0 {
|
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
|
// 4. ioctl(TIOCGWINSZ) on stdout
|
||||||
if let Some(w) = ioctl_width(libc::STDOUT_FILENO) {
|
if let Some(w) = ioctl_width(libc::STDOUT_FILENO) {
|
||||||
if w > 0 {
|
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 let Some(w) = process_tree_width() {
|
||||||
if w > 0 {
|
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 let Some(w) = stty_dev_tty() {
|
||||||
if w > 0 {
|
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(val) = std::env::var("COLUMNS") {
|
||||||
if let Ok(w) = val.parse::<u16>() {
|
if let Ok(w) = val.parse::<u16>() {
|
||||||
if w > 0 {
|
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 let Some(w) = tput_cols() {
|
||||||
if w > 0 {
|
if w > 0 {
|
||||||
return (w, "tput");
|
return Some((w, "tput"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Fallback
|
None
|
||||||
(120, "fallback")
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
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