1use crate::help;
11use crate::key::{self, Binding};
12use crate::viewport;
13use rusty_bubbletea::key::KeyPressMsg;
14use rusty_bubbletea::model::{Cmd, Msg};
15use rusty_lipgloss::{self, Style};
16use rusty_x_ansi;
17
18#[derive(Debug)]
20pub struct Model {
21 pub key_map: KeyMap,
23 pub help: help::Model,
25
26 cols: Vec<Column>,
27 rows: Vec<Row>,
28 cursor: usize,
29 focus: bool,
30 styles: Styles,
31
32 viewport: viewport::Model,
33 start: usize,
34 end: usize,
35}
36
37pub type Row = Vec<String>;
39
40#[derive(Debug, Clone)]
42pub struct Column {
43 pub title: String,
45 pub width: usize,
47}
48
49#[derive(Debug, Clone)]
52pub struct KeyMap {
53 pub line_up: Binding,
55 pub line_down: Binding,
57 pub page_up: Binding,
59 pub page_down: Binding,
61 pub half_page_up: Binding,
63 pub half_page_down: Binding,
65 pub goto_top: Binding,
67 pub goto_bottom: Binding,
69}
70
71impl KeyMap {
73 pub fn short_help(&self) -> Vec<Binding> {
75 vec![self.line_up.clone(), self.line_down.clone()]
76 }
77
78 pub fn full_help(&self) -> Vec<Vec<Binding>> {
80 vec![
81 vec![
82 self.line_up.clone(),
83 self.line_down.clone(),
84 self.goto_top.clone(),
85 self.goto_bottom.clone(),
86 ],
87 vec![
88 self.page_up.clone(),
89 self.page_down.clone(),
90 self.half_page_up.clone(),
91 self.half_page_down.clone(),
92 ],
93 ]
94 }
95}
96
97impl help::KeyMap for KeyMap {
98 fn short_help(&self) -> Vec<Binding> {
99 KeyMap::short_help(self)
100 }
101
102 fn full_help(&self) -> Vec<Vec<Binding>> {
103 KeyMap::full_help(self)
104 }
105}
106
107pub fn default_key_map() -> KeyMap {
109 KeyMap {
110 line_up: key::new_binding(vec![
111 key::with_keys(&["up", "k"]),
112 key::with_help("↑/k", "up"),
113 ]),
114 line_down: key::new_binding(vec![
115 key::with_keys(&["down", "j"]),
116 key::with_help("↓/j", "down"),
117 ]),
118 page_up: key::new_binding(vec![
119 key::with_keys(&["b", "pgup"]),
120 key::with_help("b/pgup", "page up"),
121 ]),
122 page_down: key::new_binding(vec![
123 key::with_keys(&["f", "pgdown", "space"]),
124 key::with_help("f/pgdn", "page down"),
125 ]),
126 half_page_up: key::new_binding(vec![
127 key::with_keys(&["u", "ctrl+u"]),
128 key::with_help("u", "½ page up"),
129 ]),
130 half_page_down: key::new_binding(vec![
131 key::with_keys(&["d", "ctrl+d"]),
132 key::with_help("d", "½ page down"),
133 ]),
134 goto_top: key::new_binding(vec![
135 key::with_keys(&["home", "g"]),
136 key::with_help("g/home", "go to start"),
137 ]),
138 goto_bottom: key::new_binding(vec![
139 key::with_keys(&["end", "G"]),
140 key::with_help("G/end", "go to end"),
141 ]),
142 }
143}
144
145#[derive(Debug, Clone)]
148pub struct Styles {
149 pub header: Style,
151 pub cell: Style,
153 pub selected: Style,
155}
156
157pub fn default_styles() -> Styles {
159 Styles {
160 selected: rusty_lipgloss::new_style().bold(true).foreground("212"),
161 header: rusty_lipgloss::new_style().bold(true).padding(&[0, 1]),
162 cell: rusty_lipgloss::new_style().padding(&[0, 1]),
163 }
164}
165
166pub type Option = Box<dyn FnOnce(&mut Model)>; pub fn new(opts: Vec<Option>) -> Model {
179 let mut m = Model {
180 cursor: 0,
181 viewport: viewport::new(vec![viewport::with_height(20)]),
182
183 key_map: default_key_map(),
184 help: help::new(),
185 styles: default_styles(),
186
187 cols: vec![],
188 rows: vec![],
189 focus: false,
190 start: 0,
191 end: 0,
192 };
193
194 for opt in opts {
195 opt(&mut m);
196 }
197
198 m.update_viewport();
199
200 m
201}
202
203pub fn with_columns(cols: &[Column]) -> Option {
205 let cols = cols.to_vec();
206 Box::new(move |m: &mut Model| {
207 m.cols = cols;
208 })
209}
210
211pub fn with_rows(rows: &[Row]) -> Option {
213 let rows = rows.to_vec();
214 Box::new(move |m: &mut Model| {
215 m.rows = rows;
216 })
217}
218
219pub fn with_height(h: usize) -> Option {
221 Box::new(move |m: &mut Model| {
222 let hh = rusty_lipgloss::size::height(&m.headers_view());
223 m.viewport.set_height(h - hh);
224 })
225}
226
227pub fn with_width(w: usize) -> Option {
229 Box::new(move |m: &mut Model| {
230 m.viewport.set_width(w);
231 })
232}
233
234pub fn with_focused(f: bool) -> Option {
236 Box::new(move |m: &mut Model| {
237 m.focus = f;
238 })
239}
240
241pub fn with_styles(s: Styles) -> Option {
243 Box::new(move |m: &mut Model| {
244 m.styles = s;
245 })
246}
247
248pub fn with_key_map(km: KeyMap) -> Option {
250 Box::new(move |m: &mut Model| {
251 m.key_map = km;
252 })
253}
254
255impl Model {
256 pub fn set_styles(&mut self, s: Styles) {
258 self.styles = s;
259 self.update_viewport();
260 }
261
262 pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
264 if !self.focus {
265 return None;
266 }
267
268 if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
269 let k = &m.0;
270 if key::matches(k, std::slice::from_ref(&self.key_map.line_up)) {
271 self.move_up(1);
272 } else if key::matches(k, std::slice::from_ref(&self.key_map.line_down)) {
273 self.move_down(1);
274 } else if key::matches(k, std::slice::from_ref(&self.key_map.page_up)) {
275 self.move_up(self.viewport.height());
276 } else if key::matches(k, std::slice::from_ref(&self.key_map.page_down)) {
277 self.move_down(self.viewport.height());
278 } else if key::matches(k, std::slice::from_ref(&self.key_map.half_page_up)) {
279 self.move_up(self.viewport.height() / 2);
280 } else if key::matches(k, std::slice::from_ref(&self.key_map.half_page_down)) {
281 self.move_down(self.viewport.height() / 2);
282 } else if key::matches(k, std::slice::from_ref(&self.key_map.goto_top)) {
283 self.goto_top();
284 } else if key::matches(k, std::slice::from_ref(&self.key_map.goto_bottom)) {
285 self.goto_bottom();
286 }
287 }
288
289 None
290 }
291
292 pub fn focused(&self) -> bool {
294 self.focus
295 }
296
297 pub fn focus(&mut self) {
300 self.focus = true;
301 self.update_viewport();
302 }
303
304 pub fn blur(&mut self) {
306 self.focus = false;
307 self.update_viewport();
308 }
309
310 pub fn view(&self) -> String {
312 self.headers_view() + "\n" + &self.viewport.view()
313 }
314
315 pub fn help_view(&self) -> String {
319 self.help.view(&self.key_map)
320 }
321
322 pub fn update_viewport(&mut self) {
325 let mut rendered_rows: Vec<String> = Vec::with_capacity(self.rows.len());
326
327 self.start = clamp(
332 self.cursor.saturating_sub(self.viewport.height()),
333 0,
334 self.cursor,
335 );
336 self.end = clamp(
337 self.cursor + self.viewport.height(),
338 self.cursor,
339 self.rows.len(),
340 );
341 for i in self.start..self.end {
342 rendered_rows.push(self.render_row(i));
343 }
344
345 let refs: Vec<&str> = rendered_rows.iter().map(|s| s.as_str()).collect();
346 self.viewport
347 .set_content(&rusty_lipgloss::join::join_vertical(
348 rusty_lipgloss::LEFT,
349 &refs,
350 ));
351 }
352
353 pub fn selected_row(&self) -> std::option::Option<Row> {
355 if self.cursor >= self.rows.len() {
356 return None;
357 }
358
359 Some(self.rows[self.cursor].clone())
360 }
361
362 pub fn rows(&self) -> &[Row] {
364 &self.rows
365 }
366
367 pub fn columns(&self) -> &[Column] {
369 &self.cols
370 }
371
372 pub fn set_rows(&mut self, r: &[Row]) {
374 self.rows = r.to_vec();
375
376 if self.cursor > self.rows.len().saturating_sub(1) {
377 self.cursor = self.rows.len().saturating_sub(1);
378 }
379
380 self.update_viewport();
381 }
382
383 pub fn set_columns(&mut self, c: &[Column]) {
385 self.cols = c.to_vec();
386 self.update_viewport();
387 }
388
389 pub fn set_width(&mut self, w: usize) {
391 self.viewport.set_width(w);
392 self.update_viewport();
393 }
394
395 pub fn set_height(&mut self, h: usize) {
397 let hh = rusty_lipgloss::size::height(&self.headers_view());
398 self.viewport.set_height(h - hh);
399 self.update_viewport();
400 }
401
402 pub fn height(&self) -> usize {
404 self.viewport.height()
405 }
406
407 pub fn width(&self) -> usize {
409 self.viewport.width()
410 }
411
412 pub fn cursor(&self) -> usize {
414 self.cursor
415 }
416
417 pub fn set_cursor(&mut self, n: usize) {
419 self.cursor = clamp(n, 0, self.rows.len().saturating_sub(1));
420 self.update_viewport();
421 }
422
423 pub fn move_up(&mut self, n: usize) {
426 self.cursor = clamp(
429 self.cursor.saturating_sub(n),
430 0,
431 self.rows.len().saturating_sub(1),
432 );
433
434 let mut offset = self.viewport.y_offset();
435 if self.start == 0 {
436 offset = clamp(offset, 0, self.cursor);
437 } else if self.start < self.viewport.height() {
438 offset = clamp(clamp(offset + n, 0, self.cursor), 0, self.viewport.height());
439 } else if offset >= 1 {
440 offset = clamp(offset + n, 1, self.viewport.height());
441 }
442 self.viewport.set_y_offset(offset);
443 self.update_viewport();
444 }
445
446 pub fn move_down(&mut self, n: usize) {
449 self.cursor = clamp(self.cursor + n, 0, self.rows.len().saturating_sub(1));
450 self.update_viewport();
451
452 let mut offset = self.viewport.y_offset();
453 if self.end == self.rows.len() && offset > 0 {
454 offset = clamp(offset - n, 1, self.viewport.height());
455 } else if self.cursor > (self.end - self.start) / 2 && offset > 0 {
456 offset = clamp(offset - n, 1, self.cursor);
457 } else if offset > 1 {
458 } else if self.cursor > offset + self.viewport.height() - 1 {
460 offset = clamp(offset + 1, 0, 1);
461 }
462 self.viewport.set_y_offset(offset);
463 }
464
465 pub fn goto_top(&mut self) {
467 let n = self.cursor;
468 self.move_up(n);
469 }
470
471 pub fn goto_bottom(&mut self) {
473 let n = self.rows.len();
474 self.move_down(n);
475 }
476
477 pub fn from_values(&mut self, value: &str, separator: &str) {
481 let mut rows: Vec<Row> = vec![];
482 for line in value.split('\n') {
483 let mut r: Row = vec![];
484 for field in line.split(separator) {
485 r.push(field.to_string());
486 }
487 rows.push(r);
488 }
489
490 self.set_rows(&rows);
491 }
492
493 fn headers_view(&self) -> String {
494 let mut s: Vec<String> = Vec::with_capacity(self.cols.len());
495 for col in &self.cols {
496 if col.width == 0 {
497 continue;
498 }
499 let style = rusty_lipgloss::new_style()
500 .width(col.width)
501 .max_width(col.width)
502 .inline(true);
503 let rendered_cell = style.render(&rusty_x_ansi::truncate(&col.title, col.width, "…"));
504 s.push(self.styles.header.clone().render(&rendered_cell));
505 }
506 let refs: Vec<&str> = s.iter().map(|x| x.as_str()).collect();
507 rusty_lipgloss::join::join_horizontal(rusty_lipgloss::TOP, &refs)
508 }
509
510 fn render_row(&self, r: usize) -> String {
511 let mut s: Vec<String> = Vec::with_capacity(self.cols.len());
512 for (i, value) in self.rows[r].iter().enumerate() {
513 if self.cols[i].width == 0 {
514 continue;
515 }
516 let style = rusty_lipgloss::new_style()
517 .width(self.cols[i].width)
518 .max_width(self.cols[i].width)
519 .inline(true);
520 let rendered_cell =
521 style.render(&rusty_x_ansi::truncate(value, self.cols[i].width, "…"));
522 s.push(self.styles.cell.clone().render(&rendered_cell));
523 }
524
525 let refs: Vec<&str> = s.iter().map(|x| x.as_str()).collect();
526 let row = rusty_lipgloss::join::join_horizontal(rusty_lipgloss::TOP, &refs);
527
528 if r == self.cursor {
529 return self.styles.selected.clone().render(&row);
530 }
531
532 row
533 }
534}
535
536fn clamp(v: usize, low: usize, high: usize) -> usize {
537 v.max(low).min(high)
538}