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