Skip to main content

lean_ctx/cli/
common.rs

1pub(crate) fn print_savings(original: usize, sent: usize) {
2    let footer = crate::core::protocol::format_savings(original, sent);
3    if !footer.is_empty() {
4        println!("{footer}");
5    }
6}
7
8/// Strip savings footers from daemon output when the CLI client has footer suppressed.
9#[cfg(unix)]
10pub(crate) fn filter_daemon_output(text: &str) -> String {
11    if crate::core::protocol::savings_footer_visible() {
12        return text.to_string();
13    }
14    text.lines()
15        .filter(|l| {
16            let t = l.trim();
17            !(t.starts_with('[')
18                && t.contains("tok")
19                && t.ends_with(']')
20                && (t.contains("tok saved") || t.contains("lean-ctx:") || t.contains("vs native")))
21        })
22        .collect::<Vec<_>>()
23        .join("\n")
24}
25
26pub fn load_shell_history_pub() -> Vec<String> {
27    load_shell_history()
28}
29
30pub(crate) fn load_shell_history() -> Vec<String> {
31    let shell = std::env::var("SHELL").unwrap_or_default();
32    let Some(home) = dirs::home_dir() else {
33        return Vec::new();
34    };
35
36    let history_file = if shell.contains("zsh") {
37        home.join(".zsh_history")
38    } else if shell.contains("fish") {
39        home.join(".local/share/fish/fish_history")
40    } else if cfg!(windows) && shell.is_empty() {
41        home.join("AppData")
42            .join("Roaming")
43            .join("Microsoft")
44            .join("Windows")
45            .join("PowerShell")
46            .join("PSReadLine")
47            .join("ConsoleHost_history.txt")
48    } else {
49        home.join(".bash_history")
50    };
51
52    match std::fs::read_to_string(&history_file) {
53        Ok(content) => content
54            .lines()
55            .filter_map(|l| {
56                let trimmed = l.trim();
57                if trimmed.starts_with(':') {
58                    trimmed
59                        .split(';')
60                        .nth(1)
61                        .map(std::string::ToString::to_string)
62                } else {
63                    Some(trimmed.to_string())
64                }
65            })
66            .filter(|l| !l.is_empty())
67            .collect(),
68        Err(_) => Vec::new(),
69    }
70}
71
72pub(crate) fn daemon_fallback_hint() {
73    use std::sync::Once;
74    static HINT: Once = Once::new();
75    HINT.call_once(|| {
76        if crate::core::protocol::meta_visible() {
77            eprintln!("\x1b[2;33mhint: daemon not running — stats tracked locally (lean-ctx serve -d for full tracking)\x1b[0m");
78        }
79    });
80}
81
82pub(crate) fn format_tokens_cli(tokens: u64) -> String {
83    if tokens >= 1_000_000 {
84        format!("{:.1}M", tokens as f64 / 1_000_000.0)
85    } else if tokens >= 1_000 {
86        format!("{:.1}K", tokens as f64 / 1_000.0)
87    } else {
88        format!("{tokens}")
89    }
90}
91
92pub(crate) fn cli_track_read(path: &str, mode: &str, original_tokens: usize, output_tokens: usize) {
93    crate::core::tool_lifecycle::record_file_read(
94        path,
95        mode,
96        original_tokens,
97        output_tokens,
98        false,
99    );
100}
101
102pub(crate) fn cli_track_read_cached(
103    path: &str,
104    mode: &str,
105    original_tokens: usize,
106    output_tokens: usize,
107) {
108    crate::core::tool_lifecycle::record_file_read(path, mode, original_tokens, output_tokens, true);
109}
110
111pub(crate) fn cli_track_search(original_tokens: usize, output_tokens: usize) {
112    crate::core::tool_lifecycle::record_search(original_tokens, output_tokens);
113}
114
115pub(crate) fn cli_track_tree(original_tokens: usize, output_tokens: usize) {
116    crate::core::tool_lifecycle::record_tree(original_tokens, output_tokens);
117}
118
119pub(crate) fn detect_project_root(args: &[String]) -> String {
120    let mut it = args.iter().peekable();
121    while let Some(a) = it.next() {
122        if let Some(v) = a.strip_prefix("--root=") {
123            if !v.trim().is_empty() {
124                return v.to_string();
125            }
126        }
127        if let Some(v) = a.strip_prefix("--project-root=") {
128            if !v.trim().is_empty() {
129                return v.to_string();
130            }
131        }
132        if a == "--root" || a == "--project-root" {
133            if let Some(v) = it.peek() {
134                if !v.starts_with("--") && !v.trim().is_empty() {
135                    return (*v).clone();
136                }
137            }
138        }
139    }
140    std::env::current_dir()
141        .ok()
142        .map_or_else(|| ".".to_string(), |p| p.to_string_lossy().to_string())
143}