lean_ctx/core/buddy/
format.rs1use 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}