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#[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 eprintln!("\x1b[2;33mhint: daemon not running — stats tracked locally (lean-ctx serve -d for full tracking)\x1b[0m");
77 });
78}
79
80pub(crate) fn format_tokens_cli(tokens: u64) -> String {
81 if tokens >= 1_000_000 {
82 format!("{:.1}M", tokens as f64 / 1_000_000.0)
83 } else if tokens >= 1_000 {
84 format!("{:.1}K", tokens as f64 / 1_000.0)
85 } else {
86 format!("{tokens}")
87 }
88}
89
90pub(crate) fn cli_track_read(path: &str, mode: &str, original_tokens: usize, output_tokens: usize) {
91 crate::core::tool_lifecycle::record_file_read(
92 path,
93 mode,
94 original_tokens,
95 output_tokens,
96 false,
97 );
98}
99
100pub(crate) fn cli_track_read_cached(
101 path: &str,
102 mode: &str,
103 original_tokens: usize,
104 output_tokens: usize,
105) {
106 crate::core::tool_lifecycle::record_file_read(path, mode, original_tokens, output_tokens, true);
107}
108
109pub(crate) fn cli_track_search(original_tokens: usize, output_tokens: usize) {
110 crate::core::tool_lifecycle::record_search(original_tokens, output_tokens);
111}
112
113pub(crate) fn cli_track_tree(original_tokens: usize, output_tokens: usize) {
114 crate::core::tool_lifecycle::record_tree(original_tokens, output_tokens);
115}
116
117pub(crate) fn detect_project_root(args: &[String]) -> String {
118 let mut it = args.iter().peekable();
119 while let Some(a) = it.next() {
120 if let Some(v) = a.strip_prefix("--root=") {
121 if !v.trim().is_empty() {
122 return v.to_string();
123 }
124 }
125 if let Some(v) = a.strip_prefix("--project-root=") {
126 if !v.trim().is_empty() {
127 return v.to_string();
128 }
129 }
130 if a == "--root" || a == "--project-root" {
131 if let Some(v) = it.peek() {
132 if !v.starts_with("--") && !v.trim().is_empty() {
133 return (*v).clone();
134 }
135 }
136 }
137 }
138 std::env::current_dir()
139 .ok()
140 .map_or_else(|| ".".to_string(), |p| p.to_string_lossy().to_string())
141}