Skip to main content

st/
tips.rs

1// Smart Tips System - "Helpful hints without the hassle!"
2// Shows tips at the top, detects cool terminals, and respects user preferences
3
4use 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, // Show on first run
28        }
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        // Create .st directory if it doesn't exist - our home for all persistence!
46        if let Some(parent) = state_file.parent() {
47            fs::create_dir_all(parent).ok();
48        }
49
50        // Load or create state
51        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        // Detect cool terminal
59        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        // Check for cool terminal features
70        if let Ok(term) = env::var("TERM") {
71            // Check for 256 color support or better
72            if term.contains("256color") || term.contains("truecolor") {
73                return true;
74            }
75        }
76
77        // Check for specific terminals known to be cool
78        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        // Check for Windows Terminal
86        if env::var("WT_SESSION").is_ok() {
87            return true;
88        }
89
90        // Check for cool terminal emulators via env vars
91        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        // Show on first run or when we hit the random interval
104        if self.state.run_count >= self.state.next_show_at {
105            // Set next random show between 10-20 runs
106            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; // Show on next run
126        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            // Fancy display for cool terminals
227            self.display_fancy_tip(&tip, disable_hint, terminal_width);
228        } else {
229            // Simple display for basic terminals
230            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        // Calculate how much space we have for the tip
239        let available = width.saturating_sub(hint_len + 10); // Leave some padding
240
241        // Truncate tip if needed
242        let tip_display = if tip.len() > available {
243            format!("{}...", &tip[..available.saturating_sub(3)])
244        } else {
245            tip.to_string()
246        };
247
248        // Create the fancy line
249        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
269// Get terminal width helper
270pub fn get_terminal_width() -> usize {
271    terminal_size::terminal_size()
272        .map(|(w, _)| w.0 as usize)
273        .unwrap_or(80)
274}
275
276// Public API functions for main.rs
277pub 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!(); // Extra line for spacing
284    }
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}