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