1use crate::key::Binding;
11use rusty_bubbletea::model::{Cmd, Msg};
12use rusty_lipgloss::{color::Color, style::Style, TOP};
13
14pub trait KeyMap {
22 fn short_help(&self) -> Vec<Binding>;
26
27 fn full_help(&self) -> Vec<Vec<Binding>>;
31}
32
33#[derive(Debug, Clone)]
35pub struct Styles {
36 pub ellipsis: Style,
38
39 pub short_key: Style,
41 pub short_desc: Style,
43 pub short_separator: Style,
45
46 pub full_key: Style,
48 pub full_desc: Style,
50 pub full_separator: Style,
52}
53
54pub fn default_styles(is_dark: bool) -> Styles {
57 let light_dark = rusty_lipgloss::color::light_dark(is_dark);
58
59 let key_style =
60 Style::new().foreground_color(light_dark(Color::parse("#909090"), Color::parse("#626262")));
61 let desc_style =
62 Style::new().foreground_color(light_dark(Color::parse("#B2B2B2"), Color::parse("#4A4A4A")));
63 let sep_style =
64 Style::new().foreground_color(light_dark(Color::parse("#DADADA"), Color::parse("#3C3C3C")));
65
66 Styles {
67 short_key: key_style.clone(),
68 short_desc: desc_style.clone(),
69 short_separator: sep_style.clone(),
70 ellipsis: sep_style.clone(),
71 full_key: key_style,
72 full_desc: desc_style,
73 full_separator: sep_style,
74 }
75}
76
77pub fn default_dark_styles() -> Styles {
79 default_styles(true)
80}
81
82pub fn default_light_styles() -> Styles {
84 default_styles(false)
85}
86
87#[derive(Debug, Clone)]
89pub struct Model {
90 pub show_all: bool,
92
93 pub short_separator: String,
95 pub full_separator: String,
97
98 pub ellipsis: String,
101
102 pub styles: Styles,
104
105 width: usize,
106}
107
108pub fn new() -> Model {
110 Model {
111 show_all: false,
112 short_separator: " • ".to_string(),
113 full_separator: " ".to_string(),
114 ellipsis: "…".to_string(),
115 styles: default_dark_styles(),
116 width: 0,
117 }
118}
119
120impl Model {
121 pub fn update(&mut self, _msg: &dyn Msg) -> Cmd {
123 None
124 }
125
126 pub fn view(&self, k: &dyn KeyMap) -> String {
128 if self.show_all {
129 return self.full_help_view(&k.full_help());
130 }
131 self.short_help_view(&k.short_help())
132 }
133
134 pub fn set_width(&mut self, w: usize) {
136 self.width = w;
137 }
138
139 pub fn width(&self) -> usize {
141 self.width
142 }
143
144 pub fn short_help_view(&self, bindings: &[Binding]) -> String {
148 if bindings.is_empty() {
149 return String::new();
150 }
151
152 let mut b = String::new();
153 let mut total_width = 0;
154 let separator = self
155 .styles
156 .short_separator
157 .clone()
158 .inline(true)
159 .render(&self.short_separator);
160
161 for (i, kb) in bindings.iter().enumerate() {
162 if !kb.enabled() {
163 continue;
164 }
165
166 let sep = if total_width > 0 && i < bindings.len() {
168 separator.clone()
169 } else {
170 String::new()
171 };
172
173 let str = sep
175 + &self
176 .styles
177 .short_key
178 .clone()
179 .inline(true)
180 .render(&kb.help().key)
181 + " "
182 + &self
183 .styles
184 .short_desc
185 .clone()
186 .inline(true)
187 .render(&kb.help().desc);
188 let w = rusty_lipgloss::size::width(&str);
189
190 if let (tail, false) = self.should_add_item(total_width, w) {
192 if !tail.is_empty() {
193 b.push_str(&tail);
194 }
195 break;
196 }
197
198 total_width += w;
199 b.push_str(&str);
200 }
201
202 b
203 }
204
205 pub fn full_help_view(&self, groups: &[Vec<Binding>]) -> String {
208 if groups.is_empty() {
209 return String::new();
210 }
211
212 let mut out: Vec<String> = Vec::new();
213
214 let mut total_width = 0;
215 let separator = self
216 .styles
217 .full_separator
218 .clone()
219 .inline(true)
220 .render(&self.full_separator);
221
222 for (i, group) in groups.iter().enumerate() {
224 if group.is_empty() || !should_render_column(group) {
225 continue;
226 }
227 let mut keys: Vec<String> = Vec::new();
228 let mut descriptions: Vec<String> = Vec::new();
229
230 let sep = if total_width > 0 && i < groups.len() {
232 separator.clone()
233 } else {
234 String::new()
235 };
236
237 for kb in group {
239 if !kb.enabled() {
240 continue;
241 }
242 keys.push(kb.help().key);
243 descriptions.push(kb.help().desc);
244 }
245
246 let key_col = self.styles.full_key.clone().render(&keys.join("\n"));
248 let desc_col = self
249 .styles
250 .full_desc
251 .clone()
252 .render(&descriptions.join("\n"));
253 let col = rusty_lipgloss::join::join_horizontal(TOP, &[&sep, &key_col, " ", &desc_col]);
254 let w = rusty_lipgloss::size::width(&col);
255
256 if let (tail, false) = self.should_add_item(total_width, w) {
258 if !tail.is_empty() {
259 out.push(tail);
260 }
261 break;
262 }
263
264 total_width += w;
265 out.push(col);
266 }
267
268 let refs: Vec<&str> = out.iter().map(|s| s.as_str()).collect();
269 rusty_lipgloss::join::join_horizontal(TOP, &refs)
270 }
271
272 fn should_add_item(&self, total_width: usize, width: usize) -> (String, bool) {
273 if self.width > 0 && total_width + width > self.width {
275 let tail = String::from(" ")
276 + &self
277 .styles
278 .ellipsis
279 .clone()
280 .inline(true)
281 .render(&self.ellipsis);
282
283 if total_width + rusty_lipgloss::size::width(&tail) < self.width {
284 return (tail, false);
285 }
286 }
287 (String::new(), true)
288 }
289}
290
291fn should_render_column(b: &[Binding]) -> bool {
292 b.iter().any(|v| v.enabled())
293}