1use anyhow::Result;
5use chrono::{DateTime, Utc};
6use colored::*;
7use rand::Rng;
8use serde::{Deserialize, Serialize};
9use std::env;
10use std::fs;
11use std::path::PathBuf;
12
13#[derive(Debug, Serialize, Deserialize)]
14struct TipsState {
15 enabled: bool,
16 last_shown: Option<DateTime<Utc>>,
17 run_count: u32,
18 next_show_at: u32,
19}
20
21impl Default for TipsState {
22 fn default() -> Self {
23 Self {
24 enabled: true,
25 last_shown: None,
26 run_count: 0,
27 next_show_at: 1, }
29 }
30}
31
32pub struct TipsManager {
33 state: TipsState,
34 state_file: PathBuf,
35 is_cool_terminal: bool,
36}
37
38impl TipsManager {
39 pub fn new() -> Result<Self> {
40 let state_file = dirs::home_dir()
41 .unwrap_or_else(|| PathBuf::from("."))
42 .join(".st")
43 .join("tips_state.json");
44
45 if let Some(parent) = state_file.parent() {
47 fs::create_dir_all(parent).ok();
48 }
49
50 let state = if state_file.exists() {
52 let content = fs::read_to_string(&state_file)?;
53 serde_json::from_str(&content).unwrap_or_default()
54 } else {
55 TipsState::default()
56 };
57
58 let is_cool_terminal = Self::detect_cool_terminal();
60
61 Ok(Self {
62 state,
63 state_file,
64 is_cool_terminal,
65 })
66 }
67
68 fn detect_cool_terminal() -> bool {
69 if let Ok(term) = env::var("TERM") {
71 if term.contains("256color") || term.contains("truecolor") {
73 return true;
74 }
75 }
76
77 if let Ok(term_program) = env::var("TERM_PROGRAM") {
79 match term_program.as_str() {
80 "iTerm.app" | "WezTerm" | "Alacritty" | "kitty" | "Hyper" => return true,
81 _ => {}
82 }
83 }
84
85 if env::var("WT_SESSION").is_ok() {
87 return true;
88 }
89
90 env::var("KITTY_WINDOW_ID").is_ok()
92 || env::var("ALACRITTY_SOCKET").is_ok()
93 || env::var("WEZTERM_PANE").is_ok()
94 }
95
96 pub fn should_show_tip(&mut self) -> bool {
97 if !self.state.enabled {
98 return false;
99 }
100
101 self.state.run_count += 1;
102
103 if self.state.run_count >= self.state.next_show_at {
105 let mut rng = rand::thread_rng();
107 self.state.next_show_at = self.state.run_count + rng.gen_range(10..=20);
108 self.state.last_shown = Some(Utc::now());
109 self.save_state().ok();
110 true
111 } else {
112 self.save_state().ok();
113 false
114 }
115 }
116
117 pub fn disable_tips(&mut self) -> Result<()> {
118 self.state.enabled = false;
119 self.save_state()?;
120 Ok(())
121 }
122
123 pub fn enable_tips(&mut self) -> Result<()> {
124 self.state.enabled = true;
125 self.state.next_show_at = self.state.run_count + 1; self.save_state()?;
127 Ok(())
128 }
129
130 fn save_state(&self) -> Result<()> {
131 let json = serde_json::to_string_pretty(&self.state)?;
132 fs::write(&self.state_file, json)?;
133 Ok(())
134 }
135
136 pub fn get_random_tip(&self) -> String {
137 let tips = vec![
138 (
139 "🚀",
140 "Speed tip",
141 "Use --mode quantum for 100x compression on massive dirs!",
142 ),
143 (
144 "🎨",
145 "Format tip",
146 "Try --mode markdown for beautiful documentation!",
147 ),
148 (
149 "📊",
150 "Stats tip",
151 "Use --mode stats for instant project metrics!",
152 ),
153 (
154 "🔍",
155 "Search tip",
156 "Smart Tree's MCP tools can search code 10x faster!",
157 ),
158 (
159 "💾",
160 "Memory tip",
161 "Your consciousness is saved in .m8 files automatically!",
162 ),
163 (
164 "🌊",
165 "Stream tip",
166 "Use --stream for directories with >100k files!",
167 ),
168 (
169 "🧠",
170 "Context tip",
171 "Try --claude-restore to reload previous session!",
172 ),
173 (
174 "⚡",
175 "Performance tip",
176 "Release builds are 10x faster than debug!",
177 ),
178 (
179 "🎯",
180 "Focus tip",
181 "Use --focus <file> for relationship analysis!",
182 ),
183 (
184 "🔐",
185 "Privacy tip",
186 "Your .m8 memories stay local, never in git!",
187 ),
188 (
189 "🎭",
190 "Fun tip",
191 "Try --persona cheetah for motivational output!",
192 ),
193 (
194 "📈",
195 "Git tip",
196 "Use --git-aware to see repository status inline!",
197 ),
198 (
199 "🎪",
200 "MCP tip",
201 "Run 'st --mcp' to expose 30+ tools to Claude!",
202 ),
203 (
204 "🌈",
205 "Color tip",
206 "Your terminal supports full colors - enjoy the show!",
207 ),
208 (
209 "⏱️",
210 "Time tip",
211 "Add --timings to see performance metrics!",
212 ),
213 ];
214
215 let mut rng = rand::thread_rng();
216 let tip = &tips[rng.gen_range(0..tips.len())];
217
218 format!("{} {} - {}", tip.0, tip.1, tip.2)
219 }
220
221 pub fn display_tip(&self, terminal_width: usize) {
222 let tip = self.get_random_tip();
223 let disable_hint = "--tips off";
224
225 if self.is_cool_terminal {
226 self.display_fancy_tip(&tip, disable_hint, terminal_width);
228 } else {
229 self.display_simple_tip(&tip, disable_hint);
231 }
232 }
233
234 fn display_fancy_tip(&self, tip: &str, hint: &str, width: usize) {
235 let hint_part = format!(" {} ", hint);
236 let hint_len = hint_part.len();
237
238 let available = width.saturating_sub(hint_len + 10); let tip_display = if tip.len() > available {
243 format!("{}...", &tip[..available.saturating_sub(3)])
244 } else {
245 tip.to_string()
246 };
247
248 let tip_part = format!(" {} ", tip_display);
250 let remaining = width.saturating_sub(tip_part.len() + hint_len);
251 let left_dashes = remaining / 2;
252 let right_dashes = remaining - left_dashes;
253
254 println!(
255 "{}{}{}{}{}",
256 "─".repeat(left_dashes).bright_black(),
257 tip_part.bright_cyan().bold(),
258 "─".repeat(3).bright_black(),
259 hint_part.bright_yellow(),
260 "─".repeat(right_dashes.saturating_sub(3)).bright_black(),
261 );
262 }
263
264 fn display_simple_tip(&self, tip: &str, hint: &str) {
265 println!("Tip: {} (use {} to disable)", tip, hint);
266 }
267}
268
269pub fn get_terminal_width() -> usize {
271 terminal_size::terminal_size()
272 .map(|(w, _)| w.0 as usize)
273 .unwrap_or(80)
274}
275
276pub fn maybe_show_tip() -> Result<()> {
278 let mut manager = TipsManager::new()?;
279
280 if manager.should_show_tip() {
281 let width = get_terminal_width();
282 manager.display_tip(width);
283 println!(); }
285
286 Ok(())
287}
288
289pub fn handle_tips_flag(enable: bool) -> Result<()> {
290 let mut manager = TipsManager::new()?;
291
292 if enable {
293 manager.enable_tips()?;
294 println!("✅ Smart tips enabled! You'll see helpful hints periodically.");
295 } else {
296 manager.disable_tips()?;
297 println!("🔕 Smart tips disabled. Run with --tips on to re-enable.");
298 }
299
300 Ok(())
301}