Skip to main content

lean_ctx/core/buddy/
format.rs

1use super::rpg::format_compact;
2use super::sprite::sprite_lines_for_tick;
3use super::types::BuddyState;
4
5pub fn format_buddy_block(state: &BuddyState, theme: &super::super::theme::Theme) -> String {
6    format_buddy_block_at(state, theme, None)
7}
8
9pub fn format_buddy_block_at(
10    state: &BuddyState,
11    theme: &super::super::theme::Theme,
12    tick: Option<u64>,
13) -> String {
14    let r = super::super::theme::rst();
15    let a = theme.accent.fg();
16    let m = theme.muted.fg();
17    let p = theme.primary.fg();
18    let rarity_color = state.rarity.color_code();
19
20    let info_lines = [
21        format!(
22            "{a}{}{r} | {p}{}{r} | {rarity_color}{}{r} | Lv.{}{r}",
23            state.name,
24            state.species.label(),
25            state.rarity.label(),
26            state.level,
27        ),
28        format!(
29            "{m}Mood: {} | XP: {}{r}",
30            state.mood.label(),
31            format_compact(state.xp),
32        ),
33        format!("{m}\"{}\"{r}", state.speech),
34    ];
35
36    let mut lines = Vec::with_capacity(9);
37    lines.push(String::new());
38    let sprite = sprite_lines_for_tick(state, tick);
39    for (i, sprite_line) in sprite.iter().enumerate() {
40        let info = if i < info_lines.len() {
41            &info_lines[i]
42        } else {
43            ""
44        };
45        lines.push(format!("  {p}{sprite_line}{r}  {info}"));
46    }
47    lines.push(String::new());
48    lines.join("\n")
49}
50
51pub fn format_buddy_full(state: &BuddyState, theme: &super::super::theme::Theme) -> String {
52    let rst = super::super::theme::rst();
53    let accent = theme.accent.fg();
54    let muted = theme.muted.fg();
55    let primary = theme.primary.fg();
56    let success = theme.success.fg();
57    let warn = theme.warning.fg();
58    let bold = super::super::theme::bold();
59    let rarity_color = state.rarity.color_code();
60
61    let mut out = Vec::new();
62
63    out.push(String::new());
64    out.push(format!("  {bold}{accent}Token Guardian{rst}"));
65    out.push(String::new());
66
67    for line in &state.ascii_art {
68        out.push(format!("    {primary}{line}{rst}"));
69    }
70    out.push(String::new());
71
72    out.push(format!(
73        "  {bold}{accent}{}{rst}  {muted}the {}{rst}  {rarity_color}{}{rst}  {muted}Lv.{}{rst}",
74        state.name,
75        state.species.label(),
76        state.rarity.label(),
77        state.level,
78    ));
79    out.push(format!(
80        "  {muted}Mood: {}  |  XP: {} / {}  |  Streak: {}d{rst}",
81        state.mood.label(),
82        format_compact(state.xp),
83        format_compact(state.xp_next_level),
84        state.streak_days,
85    ));
86    out.push(format!(
87        "  {muted}Tokens saved: {}  |  Bugs prevented: {}{rst}",
88        format_compact(state.tokens_saved),
89        state.bugs_prevented,
90    ));
91    out.push(String::new());
92
93    out.push(format!("  {bold}Stats{rst}"));
94    out.push(format!(
95        "  {success}Compression{rst}  {}",
96        stat_bar(state.stats.compression, theme)
97    ));
98    out.push(format!(
99        "  {warn}Vigilance  {rst}  {}",
100        stat_bar(state.stats.vigilance, theme)
101    ));
102    out.push(format!(
103        "  {primary}Endurance  {rst}  {}",
104        stat_bar(state.stats.endurance, theme)
105    ));
106    out.push(format!(
107        "  {accent}Wisdom     {rst}  {}",
108        stat_bar(state.stats.wisdom, theme)
109    ));
110    out.push(format!(
111        "  {muted}Experience {rst}  {}",
112        stat_bar(state.stats.experience, theme)
113    ));
114    out.push(String::new());
115
116    out.push(format!("  {muted}\"{}\"{rst}", state.speech));
117    out.push(String::new());
118
119    out.join("\n")
120}
121
122fn stat_bar(value: u8, theme: &super::super::theme::Theme) -> String {
123    let filled = (value as usize) / 5;
124    let empty = 20 - filled;
125    let r = super::super::theme::rst();
126    let g = theme.success.fg();
127    let m = theme.muted.fg();
128    format!(
129        "{g}{}{m}{}{r} {value}/100",
130        "█".repeat(filled),
131        "░".repeat(empty),
132    )
133}
134
135pub(super) fn detect_project_root_for_buddy() -> String {
136    if let Some(session) = super::super::session::SessionState::load_latest() {
137        if let Some(root) = session.project_root.as_deref() {
138            if !root.trim().is_empty() {
139                return root.to_string();
140            }
141        }
142        if let Some(cwd) = session.shell_cwd.as_deref() {
143            if !cwd.trim().is_empty() {
144                return super::super::protocol::detect_project_root_or_cwd(cwd);
145            }
146        }
147        if let Some(last) = session.files_touched.last() {
148            if !last.path.trim().is_empty() {
149                if let Some(parent) = std::path::Path::new(&last.path).parent() {
150                    let p = parent.to_string_lossy().to_string();
151                    return super::super::protocol::detect_project_root_or_cwd(&p);
152                }
153            }
154        }
155    }
156    std::env::current_dir()
157        .map(|p| super::super::protocol::detect_project_root_or_cwd(&p.to_string_lossy()))
158        .unwrap_or_default()
159}