mod constants;
mod format;
mod highlighting;
mod hyperlink;
mod line;
mod suggest;
use ansi_str::AnsiStr;
use unicode_width::UnicodeWidthStr;
pub use anstream::{eprint, eprintln, print, println, stderr, stdout};
pub use anstyle::Style as AnstyleStyle;
pub use constants::*;
#[cfg(all(test, feature = "syntax-highlighting"))]
pub(crate) use format::format_bash_with_gutter_at_width;
pub use format::{GUTTER_OVERHEAD, format_bash_with_gutter, format_with_gutter, wrap_styled_text};
pub use highlighting::format_toml;
pub use hyperlink::{Stream, hyperlink_stdout, strip_osc8_hyperlinks, supports_hyperlinks};
pub use line::{StyledLine, StyledString, truncate_visible};
pub use suggest::{suggest_command, suggest_command_in_dir};
use std::sync::atomic::{AtomicU8, Ordering};
static VERBOSITY: AtomicU8 = AtomicU8::new(0);
pub fn set_verbosity(level: u8) {
VERBOSITY.store(level, Ordering::Relaxed);
}
pub fn verbosity() -> u8 {
VERBOSITY.load(Ordering::Relaxed)
}
pub fn terminal_width() -> usize {
if let Some((terminal_size::Width(w), _)) =
terminal_size::terminal_size_of(std::io::stderr()).or_else(terminal_size::terminal_size)
{
return w as usize;
}
if let Ok(cols) = std::env::var("COLUMNS")
&& let Ok(width) = cols.parse::<usize>()
{
return width;
}
usize::MAX
}
pub fn terminal_width_for_statusline() -> usize {
statusline_width_fallback(terminal_width())
}
fn statusline_width_fallback(base: usize) -> usize {
#[cfg(unix)]
if base == usize::MAX
&& let Some(width) = detect_parent_tty_width()
{
return width;
}
base
}
#[cfg(unix)]
fn detect_parent_tty_width() -> Option<usize> {
use crate::shell_exec::Cmd;
let mut pid = std::process::id().to_string();
for _ in 0..10 {
let output = Cmd::new("ps")
.args(["-o", "ppid=,tty=", "-p", &pid])
.run()
.ok()?;
let info = String::from_utf8_lossy(&output.stdout);
let mut parts = info.split_whitespace();
let ppid = parts.next()?;
let tty = parts.next()?;
if !tty.is_empty() && tty != "?" && tty != "??" {
let size = Cmd::new("sh")
.args(["-c", &format!("stty size < /dev/{tty}")])
.run()
.ok()?;
let cols = String::from_utf8_lossy(&size.stdout)
.split_whitespace()
.nth(1)?
.parse::<usize>()
.ok()?;
return Some(cols * 80 / 100);
}
if ppid == "1" || ppid == "0" {
break;
}
pid = ppid.to_string();
}
None
}
pub fn visual_width(s: &str) -> usize {
s.ansi_strip().width()
}
pub fn fix_dim_after_color_reset(s: &str) -> String {
s.replace("\x1b[39m\x1b[2m", "\x1b[0m\x1b[2m")
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use super::*;
use anstyle::Style;
use unicode_width::UnicodeWidthStr;
#[test]
fn statusline_width_fallback_returns_base_when_known() {
assert_eq!(statusline_width_fallback(80), 80);
assert_eq!(statusline_width_fallback(1), 1);
}
#[test]
fn statusline_width_fallback_probes_parent_tty_when_unknown() {
let _ = statusline_width_fallback(usize::MAX);
}
#[test]
fn terminal_width_for_statusline_returns_a_width() {
let width = terminal_width_for_statusline();
assert!(width > 0);
}
#[test]
fn test_toml_formatting() {
let toml_content = r#"worktree-path = "../{{ repo }}.{{ branch }}"
[llm]
args = []
# This is a comment
[[approved-commands]]
project = "github.com/user/repo"
command = "npm install"
"#;
assert_snapshot!(format_toml(toml_content), @r#"
[107m [0m [2mworktree-path = [0m[2m[32m"../{{ repo }}.{{ branch }}"[0m
[107m [0m
[107m [0m [2m[36m[llm][0m
[107m [0m [2margs = [][0m
[107m [0m
[107m [0m [2m# This is a comment[0m
[107m [0m [2m[36m[[approved-commands]][0m
[107m [0m [2mproject = [0m[2m[32m"github.com/user/repo"[0m
[107m [0m [2mcommand = [0m[2m[32m"npm install"[0m
"#);
}
#[test]
fn test_styled_string_width() {
let s = StyledString::raw("hello");
assert_eq!(s.width(), 5);
let s = StyledString::raw("↑3 ↓2");
assert_eq!(
s.width(),
5,
"↑3 ↓2 should have width 5, not {}",
s.text.len()
);
let s = StyledString::raw("日本語");
assert_eq!(s.width(), 6);
let s = StyledString::raw("🎉");
assert_eq!(s.width(), 2); }
#[test]
fn test_styled_line_width() {
let mut line = StyledLine::new();
line.push_raw("Branch");
line.push_raw(" ");
line.push_raw("↑3 ↓2");
assert_eq!(line.width(), 13);
}
#[test]
fn test_styled_line_padding() {
let mut line = StyledLine::new();
line.push_raw("test");
assert_eq!(line.width(), 4);
line.pad_to(10);
assert_eq!(line.width(), 10);
line.pad_to(10);
assert_eq!(line.width(), 10);
}
#[test]
fn test_sparse_column_padding() {
let mut line1 = StyledLine::new();
line1.push_raw(format!("{:8}", "branch-a"));
line1.push_raw(" ");
line1.push_raw(format!("{:5}", "↑3 ↓2"));
line1.push_raw(" ");
let mut line2 = StyledLine::new();
line2.push_raw(format!("{:8}", "branch-b"));
line2.push_raw(" ");
line2.push_raw(" ".repeat(5));
line2.push_raw(" ");
assert_eq!(
line1.width(),
line2.width(),
"Rows with and without sparse column data should have same width"
);
}
#[test]
fn test_wrap_text_no_wrapping_needed() {
let result = super::format::wrap_text_at_width("short line", 50);
assert_eq!(result, vec!["short line"]);
}
#[test]
fn test_wrap_text_at_word_boundary() {
let text = "This is a very long line that needs to be wrapped at word boundaries";
let result = super::format::wrap_text_at_width(text, 30);
assert!(result.len() > 1);
for line in &result {
assert!(
line.width() <= 30 || !line.contains(' '),
"Line '{}' has width {} which exceeds 30 and contains spaces",
line,
line.width()
);
}
let rejoined = result.join(" ");
assert_eq!(
rejoined.split_whitespace().collect::<Vec<_>>(),
text.split_whitespace().collect::<Vec<_>>()
);
}
#[test]
fn test_wrap_text_single_long_word() {
let result = super::format::wrap_text_at_width("verylongwordthatcannotbewrapped", 10);
assert_eq!(result.len(), 1);
assert_eq!(result[0], "verylongwordthatcannotbewrapped");
}
#[test]
fn test_wrap_text_empty_input() {
let result = super::format::wrap_text_at_width("", 50);
assert_eq!(result, vec![""]);
}
#[test]
fn test_wrap_text_unicode() {
let text = "This line has emoji 🎉 and should wrap correctly when needed";
let result = super::format::wrap_text_at_width(text, 30);
assert!(result.len() > 1);
let rejoined = result.join(" ");
assert!(rejoined.contains("🎉"));
}
#[test]
fn test_format_with_gutter_preserves_newlines() {
assert_snapshot!(format_with_gutter("Line 1\nLine 2\nLine 3", Some(80)), @"
[107m [0m Line 1
[107m [0m Line 2
[107m [0m Line 3
");
}
#[test]
fn test_format_with_gutter_long_paragraph() {
let commit_msg = "This commit refactors the authentication system to use a more secure token-based approach instead of the previous session-based system which had several security vulnerabilities that were identified during the security audit last month. The new implementation follows industry best practices and includes proper token rotation and expiration handling.";
let result = format_with_gutter(commit_msg, Some(80));
assert_snapshot!(result, @"
[107m [0m This commit refactors the authentication system to use a more secure
[107m [0m token-based approach instead of the previous session-based system which had
[107m [0m several security vulnerabilities that were identified during the security
[107m [0m audit last month. The new implementation follows industry best practices and
[107m [0m includes proper token rotation and expiration handling.
");
}
#[test]
fn test_bash_gutter_formatting_ends_with_reset() {
let command = "pre-commit run --all-files";
let result = format_bash_with_gutter(command);
assert!(
result.ends_with("\x1b[0m"),
"Bash gutter formatting should end with ANSI reset code, got: {:?}",
result.chars().rev().take(20).collect::<String>()
);
assert!(
!result.ends_with('\n'),
"Bash gutter formatting should not have trailing newline"
);
let multi_line_command = "npm install && \\\n npm run build";
let multi_result = format_bash_with_gutter(multi_line_command);
for line in multi_result.lines() {
if !line.is_empty() {
assert!(
line.contains("\x1b[0m"),
"Each line should contain ANSI reset code, line: {:?}",
line
);
}
}
assert!(
multi_result.ends_with("\x1b[0m"),
"Multi-line bash gutter formatting should end with ANSI reset"
);
}
#[test]
fn test_reset_code_behavior() {
let style_reset = format!("{:#}", Style::new());
assert_eq!(
style_reset, "",
"Style::new() with {{:#}} produces empty string (this is why we had color leaking!)"
);
let anstyle_reset = format!("{}", anstyle::Reset);
assert_eq!(
anstyle_reset, "\x1b[0m",
"anstyle::Reset produces proper ESC[0m reset code"
);
assert_ne!(
style_reset, anstyle_reset,
"Style::new() and anstyle::Reset are NOT equivalent - always use anstyle::Reset"
);
}
#[test]
fn test_wrap_text_with_ansi_codes() {
use super::format::wrap_text_at_width;
let colored_text = "* \x1b[33m9452817\x1b[m Clarify wt merge worktree removal behavior";
let result = wrap_text_at_width(colored_text, 60);
assert_eq!(
result.len(),
1,
"Colored text should NOT wrap when visual width (52) < max_width (60)"
);
assert_eq!(
result[0], colored_text,
"Should return original text with ANSI codes intact"
);
let result = wrap_text_at_width(colored_text, 30);
assert!(
result.len() > 1,
"Should wrap into multiple lines when visual width (52) > max_width (30)"
);
}
#[test]
fn test_wrap_styled_text_no_wrapping_needed() {
let result = wrap_styled_text("short line", 50);
assert_eq!(result, vec!["short line"]);
}
#[test]
fn test_wrap_styled_text_at_word_boundary() {
let text = "This is a very long line that needs wrapping";
let result = wrap_styled_text(text, 20);
assert!(result.len() > 1);
for line in &result {
let visual = visual_width(line);
assert!(
visual <= 20 || !line.contains(' '),
"Line '{}' has visual width {} which exceeds 20",
line,
visual
);
}
}
#[test]
fn test_wrap_styled_text_preserves_styles_across_breaks() {
let bold = Style::new().bold();
let input = format!("{bold}This is bold text that will wrap{bold:#}");
let result = wrap_styled_text(&input, 15);
assert!(result.len() > 1);
assert!(
result[0].contains("\x1b[1m"),
"First line should have bold code"
);
}
#[test]
fn test_wrap_styled_text_single_long_word() {
let result = wrap_styled_text("verylongwordthatcannotbewrapped", 10);
assert_eq!(result.len(), 1);
assert_eq!(result[0], "verylongwordthatcannotbewrapped");
}
#[test]
fn test_wrap_styled_text_preserves_dim_across_wrap_points() {
let dim = Style::new().dimmed();
let reset = anstyle::Reset;
let cmd_style = Style::new()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Blue)))
.bold()
.dimmed();
let styled = format!(
"{dim}{cmd_style}cp{reset}{dim} -cR {{{{ repo_root }}}}/target/debug/build {{{{ worktree }}}}/target/debug/"
);
let result = wrap_styled_text(&styled, 40);
assert!(
result.len() > 1,
"Should wrap into multiple lines, got {} lines: {:?}",
result.len(),
result
);
let dim_code = "\x1b[2m";
for (i, line) in result.iter().enumerate() {
assert!(
line.starts_with(dim_code),
"Line {} should START with dim code, but got: {:?}",
i + 1,
&line[..line.len().min(30)]
);
}
}
#[test]
fn test_format_bash_with_gutter_template_command() {
let command = "cp -cR {{ repo_root }}/target/debug/.fingerprint {{ repo_root }}/target/debug/build {{ worktree }}/target/debug/";
let result = format_bash_with_gutter_at_width(command, 80);
assert_snapshot!(result);
}
#[test]
fn test_format_bash_multiline_command_consistent_styling() {
let multiline_command = r#"[ -d {{ repo_root }}/target/debug/deps ] && [ ! -e {{ worktree }}/target ] &&
mkdir -p {{ worktree }}/target/debug/deps &&
cp -c {{ repo_root }}/target/debug/deps/*.rlib {{ repo_root }}/target/debug/deps/*.rmeta {{ worktree
}}/target/debug/deps/ &&
cp -cR {{ repo_root }}/target/debug/.fingerprint {{ repo_root }}/target/debug/build {{ worktree
}}/target/debug/"#;
let result = format_bash_with_gutter_at_width(multiline_command, 80);
assert_snapshot!(result);
}
#[test]
fn test_unhighlighted_text_has_consistent_dim_across_lines() {
assert_snapshot!(format_bash_with_gutter("echo {{ worktree\n}}/path"));
}
#[test]
fn test_syntax_highlighting_produces_multiple_colors() {
let command = "echo 'hello' | grep hello > output.txt && cat output.txt";
assert_snapshot!(format_bash_with_gutter(command));
}
#[test]
fn test_no_color_discontinuity_in_template_variables() {
let command = r#"[ -d {{ repo_root }}/target/debug/deps ] && [ ! -e {{ worktree }}/target ] &&
mkdir -p {{ worktree }}/target/debug/deps &&
cp -c {{ repo_root }}/target/debug/deps/*.rlib {{ repo_root }}/target/debug/deps/*.rmeta {{ worktree
}}/target/debug/deps/ &&
cp -cR {{ repo_root }}/target/debug/.fingerprint {{ repo_root }}/target/debug/build {{ worktree
}}/target/debug/"#;
let result = format_bash_with_gutter(command);
assert!(
!result.contains("\x1b[39m"),
"Output should NOT contain [39m (foreground reset) - this indicates wrap_ansi discontinuity.\n\
Found [39m in output:\n{}",
result
.lines()
.filter(|line| line.contains("\x1b[39m"))
.collect::<Vec<_>>()
.join("\n")
);
assert!(
!result.contains("\x1b[49m"),
"Output should NOT contain [49m (background reset)"
);
assert!(
result.contains("\x1b[2m"),
"Output should contain [2m (dim)"
);
assert!(
result.contains("\x1b[0m"),
"Output should contain [0m (reset)"
);
}
#[test]
fn test_no_bold_dim_conflict() {
let command = "cp -cR path/to/source path/to/dest";
let result = format_bash_with_gutter(command);
assert!(
!result.contains("\x1b[2m\x1b[1m\x1b[2m"),
"Output should NOT contain [2m][1m][2m] - this indicates redundant dim in token styles.\n\
Token styles should not include .dimmed() since the line already starts dim.\n\
Found pattern in output:\n{:?}",
result
);
}
#[test]
fn test_all_tokens_are_dimmed() {
let command = "cp -cR path/to/source path/to/dest";
let result = format_bash_with_gutter(command);
assert!(
result.contains("\x1b[0m\x1b[2m\x1b[34m"),
"Commands should be dim+blue [0m][2m][34m].\n\
Output:\n{:?}",
result
);
assert!(
result.contains("\x1b[0m\x1b[2m\x1b[36m"),
"Flags should be dim+cyan [0m][2m][36m].\n\
Output:\n{:?}",
result
);
assert!(
!result.contains("\x1b[1m"),
"Output should NOT contain [1m] (bold) - we use dim instead.\n\
Output:\n{:?}",
result
);
}
#[test]
fn test_fix_dim_after_color_reset() {
assert_eq!(
fix_dim_after_color_reset("\x1b[39m\x1b[2m"),
"\x1b[0m\x1b[2m"
);
assert_eq!(
fix_dim_after_color_reset("\x1b[36m?\x1b[39m\x1b[2m^\x1b[22m"),
"\x1b[36m?\x1b[0m\x1b[2m^\x1b[22m"
);
assert_eq!(
fix_dim_after_color_reset("a\x1b[39m\x1b[2mb\x1b[39m\x1b[2mc"),
"a\x1b[0m\x1b[2mb\x1b[0m\x1b[2mc"
);
assert_eq!(fix_dim_after_color_reset("no escapes"), "no escapes");
assert_eq!(
fix_dim_after_color_reset("\x1b[39m\x1b[1m"),
"\x1b[39m\x1b[1m"
);
}
}