pub mod commands;
pub mod doctor;
pub mod hooks;
use clap::ValueEnum;
use crossterm::tty::IsTty;
use std::io::stdout;
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum HostFormat {
Claude,
Gemini,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum QueryFormat {
Table,
Json,
Csv,
}
#[derive(Debug, Clone)]
pub struct ColorConfig {
pub enabled: bool,
}
impl ColorConfig {
#[must_use]
pub fn new(nocolor: bool) -> Self {
Self {
enabled: !nocolor && stdout().is_tty(),
}
}
#[must_use]
pub fn green(&self, s: &str) -> String {
if self.enabled {
format!("\x1b[32m{s}\x1b[0m")
} else {
s.to_string()
}
}
#[must_use]
pub fn blue(&self, s: &str) -> String {
if self.enabled {
format!("\x1b[34m{s}\x1b[0m")
} else {
s.to_string()
}
}
#[must_use]
pub fn red(&self, s: &str) -> String {
if self.enabled {
format!("\x1b[31m{s}\x1b[0m")
} else {
s.to_string()
}
}
#[must_use]
pub fn cyan(&self, s: &str) -> String {
if self.enabled {
format!("\x1b[36m{s}\x1b[0m")
} else {
s.to_string()
}
}
#[must_use]
pub fn yellow(&self, s: &str) -> String {
if self.enabled {
format!("\x1b[33m{s}\x1b[0m")
} else {
s.to_string()
}
}
#[must_use]
pub fn bold(&self, s: &str) -> String {
if self.enabled {
format!("\x1b[1m{s}\x1b[0m")
} else {
s.to_string()
}
}
#[must_use]
pub fn dim(&self, s: &str) -> String {
if self.enabled {
format!("\x1b[2m{s}\x1b[0m")
} else {
s.to_string()
}
}
}
#[must_use]
pub fn terminal_width() -> usize {
crossterm::terminal::size()
.map(|(w, _)| w as usize)
.unwrap_or(80)
}
#[must_use]
pub fn truncate(s: &str, max_len: usize) -> String {
if max_len <= 3 {
return ".".repeat(max_len.min(3));
}
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len - 3])
}
}
#[derive(Debug)]
pub struct ColumnWidths {
pub row_num: usize,
pub id: usize,
pub pid: usize,
pub client: usize,
pub workspace: usize,
pub started: usize,
}
impl ColumnWidths {
#[must_use]
pub const fn calculate(term_width: usize) -> Self {
let row_num = 3; let pid = 8; let started = 12;
let fixed_space = row_num + pid + started + 5;
let flexible_space = term_width.saturating_sub(fixed_space);
let min_id = 12;
let min_client = 20;
let min_workspace = 20;
let total_min_flex = min_id + min_client + min_workspace;
if flexible_space <= total_min_flex {
Self {
row_num,
id: min_id,
pid,
client: min_client,
workspace: min_workspace,
started,
}
} else {
let extra = flexible_space - total_min_flex;
Self {
row_num,
id: min_id,
pid,
client: min_client,
workspace: min_workspace + extra,
started,
}
}
}
}
#[cfg(test)]
#[allow(
clippy::expect_used,
reason = "tests use expect for readable assertions"
)]
mod tests {
use super::*;
#[test]
fn test_truncate_short_string() {
assert_eq!(truncate("hello", 10), "hello");
assert_eq!(truncate("test", 4), "test");
}
#[test]
fn test_truncate_long_string() {
assert_eq!(truncate("hello world", 8), "hello...");
assert_eq!(truncate("abcdefghij", 7), "abcd...");
}
#[test]
fn test_truncate_edge_cases() {
assert_eq!(truncate("hello", 3), "...");
assert_eq!(truncate("hello", 2), "..");
assert_eq!(truncate("hello", 1), ".");
assert_eq!(truncate("hello", 0), "");
}
#[test]
fn test_color_config_disabled() {
let config = ColorConfig::new(true);
assert!(!config.enabled);
assert_eq!(config.green("test"), "test");
assert_eq!(config.blue("test"), "test");
assert_eq!(config.red("test"), "test");
assert_eq!(config.cyan("test"), "test");
}
#[test]
fn test_calculate_column_widths() {
let widths = ColumnWidths::calculate(120);
assert_eq!(widths.row_num, 3);
assert_eq!(widths.pid, 8);
assert_eq!(widths.started, 12);
assert!(widths.id >= 12);
assert!(widths.workspace >= 20);
assert!(widths.client >= 20);
}
#[test]
fn test_calculate_column_widths_shrinks() {
let widths = ColumnWidths::calculate(60);
assert_eq!(widths.id, 12);
assert_eq!(widths.workspace, 20);
assert_eq!(widths.client, 20);
}
#[test]
fn test_calculate_column_widths_wide() {
let widths = ColumnWidths::calculate(200);
assert!(
widths.workspace > widths.client,
"workspace ({}) should be wider than client ({})",
widths.workspace,
widths.client,
);
assert_eq!(widths.client, 20);
}
}