1use std::{
2 any::Any,
3 collections::BTreeMap,
4 fmt::{Debug, Display},
5 ops::ControlFlow,
6};
7
8use crossterm::{
9 cursor::Show,
10 event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
11};
12use ratatui::{
13 prelude::{Constraint, CrosstermBackend, Layout, Rect},
14 style::{Color, Modifier, Style, Stylize},
15 symbols,
16 text::Line,
17 widgets::{Block, Borders, List, ListItem, ListState, Tabs},
18 Frame, Terminal,
19};
20
21#[derive(Debug)]
22pub enum Retning {
23 Up,
24 Down,
25 Left,
26 Right,
27}
28
29impl TryFrom<KeyEvent> for Retning {
30 type Error = ();
31
32 fn try_from(value: KeyEvent) -> Result<Self, Self::Error> {
33 match value.code {
34 KeyCode::Left => Ok(Self::Left),
35 KeyCode::Right => Ok(Self::Right),
36 KeyCode::Up => Ok(Self::Up),
37 KeyCode::Down => Ok(Self::Down),
38 KeyCode::Char('k') => Ok(Self::Up),
39 KeyCode::Char('j') => Ok(Self::Down),
40 KeyCode::Char('h') => Ok(Self::Left),
41 KeyCode::Char('l') => Ok(Self::Right),
42 _ => Err(()),
43 }
44 }
45}
46
47pub fn with_modifier(value: KeyEvent) -> Option<Retning> {
48 if value.modifiers.contains(KeyModifiers::ALT) {
49 return Retning::try_from(value).ok();
50 }
51 None
52}
53
54type Term = ratatui::Terminal<Bakende>;
55type Bakende = ratatui::backend::CrosstermBackend<std::io::Stderr>;
56
57pub struct App<T> {
58 app_state: T,
59 terminal: Term,
60 tab_idx: usize,
61 tabs: Vec<Box<dyn Tab<AppState = T>>>,
62 widget_area: Rect,
63}
64
65impl<T> App<T> {
66 pub fn new(app_data: T, tabs: Vec<Box<dyn Tab<AppState = T>>>) -> Self {
67 let terminal = Terminal::new(CrosstermBackend::new(std::io::stderr())).unwrap();
68
69 assert!(!tabs.is_empty());
70
71 Self {
72 terminal,
73 app_state: app_data,
74 tabs,
75 tab_idx: 0,
76 widget_area: Rect::default(),
77 }
78 }
79
80 pub fn run(&mut self) {
81 crossterm::terminal::enable_raw_mode().unwrap();
82 crossterm::execute!(
83 std::io::stderr(),
84 crossterm::terminal::EnterAlternateScreen,
85 Show
86 )
87 .unwrap();
88
89 loop {
90 self.draw();
91
92 match self.handle_key() {
93 ControlFlow::Continue(_) => continue,
94 ControlFlow::Break(_) => break,
95 }
96 }
97
98 crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap();
99 crossterm::terminal::disable_raw_mode().unwrap();
100 }
101
102 pub fn draw(&mut self) {
103 let idx = self.tab_idx;
104
105 self.terminal
106 .draw(|f| {
107 let (tab_area, remainder_area) = {
108 let chunks = Layout::default()
109 .direction(ratatui::prelude::Direction::Vertical)
110 .constraints(vec![Constraint::Length(3), Constraint::Min(0)])
111 .split(f.size())
112 .to_vec();
113 (chunks[0], chunks[1])
114 };
115
116 let tabs = Tabs::new(self.tabs.iter().map(|tab| tab.title()).collect())
117 .block(Block::default().borders(Borders::ALL))
118 .style(Style::default().white())
119 .highlight_style(Style::default().light_red())
120 .select(idx)
121 .divider(symbols::DOT);
122
123 f.render_widget(tabs, tab_area);
124
125 self.tabs[self.tab_idx].entry_render(f, &mut self.app_state, remainder_area);
126 self.widget_area = remainder_area;
127 })
128 .unwrap();
129 }
130
131 pub fn handle_key(&mut self) -> ControlFlow<()> {
132 let key = event::read().unwrap();
133
134 if let Event::Key(x) = key {
135 if x.code == KeyCode::Tab {
136 self.go_right()
137 } else if x.code == KeyCode::BackTab {
138 self.go_left()
139 };
140 }
141
142 let tab = &mut self.tabs[self.tab_idx];
143
144 if !tab.tabdata().is_selected && tab.tabdata().popup.is_none() {
145 if let Event::Key(k) = key {
146 if k.code == KeyCode::Char('Q') {
147 return ControlFlow::Break(());
148 }
149 }
150 }
151
152 tab.entry_keyhandler(key, &mut self.app_state, self.widget_area);
153
154 ControlFlow::Continue(())
155 }
156
157 fn go_right(&mut self) {
158 self.tab_idx = std::cmp::min(self.tab_idx + 1, self.tabs.len() - 1);
159 }
160
161 fn go_left(&mut self) {
162 if self.tab_idx != 0 {
163 self.tab_idx -= 1;
164 }
165 }
166}
167
168#[derive(Debug, Clone, Copy, Default)]
169pub struct Pos {
170 pub x: u16,
171 pub y: u16,
172}
173
174impl Pos {
175 pub fn new(x: u16, y: u16) -> Self {
176 Self { x, y }
177 }
178}
179
180#[derive(Default)]
181pub struct TabData<T> {
182 pub cursor: Pos,
183 pub is_selected: bool,
184 pub popup_state: PopUpState,
185 pub popup: Option<Box<dyn Tab<AppState = T>>>,
186 pub state_modifier: Option<Box<dyn FnMut(&Box<dyn Any>)>>,
187 pub area_map: BTreeMap<String, Rect>,
188 pub first_pass: bool,
189 pub key_history: Vec<KeyCode>,
190}
191
192pub struct Wrapper(KeyCode);
193
194impl From<KeyCode> for Wrapper {
195 fn from(value: KeyCode) -> Self {
196 Self(value)
197 }
198}
199
200impl From<char> for Wrapper {
201 fn from(c: char) -> Self {
202 Self(KeyCode::Char(c))
203 }
204}
205
206impl From<Wrapper> for KeyCode {
207 fn from(value: Wrapper) -> Self {
208 value.0
209 }
210}
211
212impl<T> Debug for TabData<T> {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 f.debug_struct("TabData")
215 .field("cursor", &self.cursor)
216 .field("is_selected", &self.is_selected)
217 .field("popup_state", &self.popup_state)
218 .finish()
219 }
220}
221
222impl<T> TabData<T> {
223 pub fn _debug_show_cursor(&self, f: &mut Frame) {
224 f.set_cursor(self.cursor.x, self.cursor.y);
225 }
226
227 pub fn is_selected(&self, area: Rect) -> bool {
228 Self::isitselected(area, self.cursor)
229 }
230
231 pub fn char_match(&self, keys: &str) -> bool {
232 let keys: Vec<KeyCode> = keys.chars().map(KeyCode::Char).collect();
233 self.key_match(keys)
234 }
235
236 pub fn key_match(&self, keys: Vec<KeyCode>) -> bool {
237 if self.key_history.len() < keys.len() {
238 return false;
239 }
240
241 self.key_history.ends_with(keys.as_slice())
242 }
243
244 fn insert_key(&mut self, key: KeyCode) {
245 let max_buffer = 30;
246 let min_buffer = 10;
247
248 if self.key_history.len() > max_buffer {
249 self.key_history.drain(..(max_buffer - min_buffer));
250 }
251
252 self.key_history.push(key);
253 }
254
255 fn is_valid_pos(&self, pos: Pos) -> bool {
256 for area in self.area_map.values() {
257 if Self::isitselected(*area, pos) {
258 return true;
259 }
260 }
261 false
262 }
263
264 pub fn move_right(&mut self) {
265 let current_area = self.current_area();
266 let new_pos = Pos {
267 x: current_area.right(),
268 y: self.cursor.y,
269 };
270 if self.is_valid_pos(new_pos) {
271 self.cursor = new_pos;
272 }
273 }
274
275 pub fn move_down(&mut self) {
276 let current_area = self.current_area();
277 let new_pos = Pos {
278 y: current_area.bottom(),
279 x: self.cursor.x,
280 };
281 if self.is_valid_pos(new_pos) {
282 self.cursor = new_pos;
283 }
284 }
285
286 fn current_area(&self) -> Rect {
287 let cursor = self.cursor;
288 for (_, area) in self.area_map.iter() {
289 if TabData::<()>::isitselected(*area, cursor) {
290 return *area;
291 }
292 }
293 panic!("omg: {:?}", cursor);
294 }
295
296 pub fn isitselected(area: Rect, cursor: Pos) -> bool {
297 cursor.x >= area.left()
298 && cursor.x < area.right()
299 && cursor.y >= area.top()
300 && cursor.y < area.bottom()
301 }
302
303 pub fn move_up(&mut self) {
304 let current_area = self.current_area();
305 let new_pos = Pos {
306 x: self.cursor.x,
307 y: current_area.top().saturating_sub(1),
308 };
309 if self.is_valid_pos(new_pos) {
310 self.cursor = new_pos;
311 }
312 }
313
314 pub fn move_left(&mut self) {
315 let current_area = self.current_area();
316 let new_pos = Pos {
317 x: current_area.left().saturating_sub(1),
318 y: self.cursor.y,
319 };
320 if self.is_valid_pos(new_pos) {
321 self.cursor = new_pos;
322 }
323 }
324
325 pub fn navigate(&mut self, direction: Retning) {
326 match direction {
327 Retning::Up => self.move_up(),
328 Retning::Down => self.move_down(),
329 Retning::Left => self.move_left(),
330 Retning::Right => self.move_right(),
331 }
332 }
333}
334
335pub enum PopUpState {
336 Exit,
337 Continue,
338 Resolve(Box<dyn Any>),
339}
340
341impl Default for PopUpState {
342 fn default() -> Self {
343 Self::Continue
344 }
345}
346
347impl Debug for PopUpState {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 match self {
350 Self::Exit => write!(f, "Exit"),
351 Self::Continue => write!(f, "Continue"),
352 Self::Resolve(arg0) => f.debug_tuple("Resolve").field(arg0).finish(),
353 }
354 }
355}
356
357pub trait Widget {
358 type AppData;
359
360 fn keyhandler(&mut self, app_data: &mut Self::AppData, key: KeyEvent);
361 fn render(&mut self, f: &mut Frame, app_data: &mut Self::AppData, area: Rect);
362
363 fn id(&self) -> String {
364 format!("{:p}", self)
365 }
366
367 fn main_render(
368 &mut self,
369 f: &mut Frame,
370 app_data: &mut Self::AppData,
371 is_selected: bool,
372 cursor: Pos,
373 area: Rect,
374 ) {
375 let rect = self.draw_titled_border(f, is_selected, cursor, area);
376 self.render(f, app_data, rect);
377 }
378
379 fn title(&self) -> &str {
380 ""
381 }
382
383 fn draw_titled_border(
384 &self,
385 f: &mut Frame,
386 is_selected: bool,
387 cursor: Pos,
388 area: Rect,
389 ) -> Rect {
390 let block = Block::default().title(self.title()).borders(Borders::ALL);
391
392 let block = if TabData::<Self::AppData>::isitselected(area, cursor) {
393 if is_selected {
394 block.border_style(Style {
395 fg: Some(ratatui::style::Color::Red),
396 ..Default::default()
397 })
398 } else {
399 block.border_style(Style {
400 fg: Some(ratatui::style::Color::Black),
401 ..Default::default()
402 })
403 }
404 } else {
405 block.border_style(Style {
406 fg: Some(ratatui::style::Color::White),
407 ..Default::default()
408 })
409 };
410
411 if area.width < 3 || area.height < 3 {
412 return area;
413 }
414
415 f.render_widget(block, area);
416
417 Rect {
418 x: area.x + 1,
419 y: area.y + 1,
420 width: area.width.saturating_sub(2),
421 height: area.height.saturating_sub(2),
422 }
423 }
424}
425
426pub trait Tab {
427 type AppState;
428
429 fn widgets(&mut self, area: Rect) -> Vec<(&mut dyn Widget<AppData = Self::AppState>, Rect)>;
432 fn tabdata(&mut self) -> &mut TabData<Self::AppState>;
433 fn tabdata_ref(&self) -> &TabData<Self::AppState>;
434 fn title(&self) -> &str;
435 fn remove_popup_hook(&mut self) {}
436
437 fn resolve_tab(&mut self, value: Box<dyn Any>) {
440 if let Some(mut fun) = std::mem::take(&mut self.tabdata().state_modifier) {
441 fun(&value);
442 }
443
444 *self.popup_state() = PopUpState::Resolve(value);
445 }
446
447 fn exit_tab(&mut self) {
448 *self.popup_state() = PopUpState::Exit;
449 }
450
451 fn set_popup(&mut self, pop: Box<dyn Tab<AppState = Self::AppState>>) {
452 self.tabdata().popup = Some(pop);
453 }
454
455 fn set_popup_with_modifier(
456 &mut self,
457 mut pop: Box<dyn Tab<AppState = Self::AppState>>,
458 f: Box<dyn FnMut(&Box<dyn Any>)>,
459 ) {
460 pop.tabdata().state_modifier = Some(f);
461 self.tabdata().popup = Some(pop);
462 }
463
464 fn move_to_widget(&mut self, w: &dyn Widget<AppData = Self::AppState>) {
465 let id = w.id();
466 self.move_to_id(id.as_str());
467 }
468
469 fn move_to_id(&mut self, id: &str) {
470 let area = self.tabdata().area_map[id];
471 self.move_to_area(area);
472 }
473
474 fn move_to_area(&mut self, area: Rect) {
475 let x = area.x + area.width / 2;
476 let y = area.y + area.height / 2;
477 self.tabdata().cursor = Pos::new(x, y);
478 }
479
480 fn handle_popup_value(&mut self, _app_data: &mut Self::AppState, _return_value: Box<dyn Any>) {}
481
482 fn tab_keyhandler_deselected(
484 &mut self,
485 _app_data: &mut Self::AppState,
486 _key: crossterm::event::KeyEvent,
487 ) -> bool {
488 true
489 }
490
491 fn tab_keyhandler_selected(
495 &mut self,
496 _app_data: &mut Self::AppState,
497 _key: crossterm::event::KeyEvent,
498 ) -> bool {
499 true
500 }
501
502 fn is_selected(&self, w: &dyn Widget<AppData = Self::AppState>) -> bool {
503 let id = w.id();
504 let Some(area) = self.tabdata_ref().area_map.get(&id) else {
505 return false;
506 };
507
508 TabData::<()>::isitselected(*area, self.tabdata_ref().cursor)
509 }
510
511 fn pre_render_hook(&mut self, _app_data: &mut Self::AppState) {}
512
513 fn render(&mut self, f: &mut ratatui::Frame, app_data: &mut Self::AppState, area: Rect) {
514 let is_selected = self.selected();
515 let cursor = self.cursor();
516
517 for (widget, area) in self.widgets(area) {
518 widget.main_render(f, app_data, is_selected, cursor, area);
519 }
520 }
521
522 fn after_keyhandler(&mut self, _app_data: &mut Self::AppState) {}
523
524 fn pop_up(&mut self) -> Option<&mut Box<dyn Tab<AppState = Self::AppState>>> {
527 self.tabdata().popup.as_mut()
528 }
529
530 fn get_popup_value(&mut self) -> Option<&mut PopUpState> {
531 self.pop_up().map(|x| x.popup_state())
532 }
533
534 fn popup_state(&mut self) -> &mut PopUpState {
535 &mut self.tabdata().popup_state
536 }
537
538 fn validate_pos(&mut self, area: Rect) {
539 let cursor = self.tabdata().cursor;
540 for (_, area) in self.widgets(area) {
541 if TabData::<()>::isitselected(area, cursor) {
542 return;
543 }
544 }
545 let the_area = self.widgets(area)[0].1;
546 self.move_to_area(the_area);
547 }
548
549 fn remove_popup(&mut self) {
551 self.tabdata().popup = None;
552 }
553
554 fn check_popup_value(&mut self, app_data: &mut Self::AppState) {
555 let mut is_exit = false;
556 let mut is_resolve = false;
557
558 let Some(popval) = self.get_popup_value() else {
559 return;
560 };
561
562 match popval {
563 PopUpState::Exit => is_exit = true,
564 PopUpState::Continue => return,
565 PopUpState::Resolve(_) => is_resolve = true,
566 }
567
568 if is_exit {
569 self.remove_popup();
570 return;
571 }
572
573 if is_resolve {
575 let PopUpState::Resolve(resolved_value) = std::mem::take(popval) else {
576 panic!()
577 };
578
579 self.handle_popup_value(app_data, resolved_value);
580 self.tabdata().popup = None;
581 }
582 }
583
584 fn pre_keyhandler_hook(&mut self, _key: KeyEvent) {}
585
586 fn entry_keyhandler(&mut self, event: Event, app_data: &mut Self::AppState, area: Rect) {
587 let Event::Key(key) = event else {
588 return;
589 };
590
591 if let Some(popup) = self.pop_up() {
592 popup.entry_keyhandler(event, app_data, area);
593 return;
594 }
595
596 self.pre_keyhandler_hook(key);
597
598 if self.selected() {
599 if key.code == KeyCode::Esc {
600 self.tabdata().is_selected = false;
601 } else if self.tab_keyhandler(app_data, key) {
602 self.widget_keyhandler(app_data, key, area);
603 }
604 } else {
605 if key.code == KeyCode::Enter {
606 self.tabdata().is_selected = true;
607 } else if key.code == KeyCode::Esc {
608 self.exit_tab();
609 } else if let Ok(ret) = Retning::try_from(key) {
610 self.navigate(ret);
611 } else {
612 self.tab_keyhandler(app_data, key);
613 }
614 }
615
616 self.after_keyhandler(app_data);
617 }
618
619 fn tab_keyhandler(
623 &mut self,
624 app_data: &mut Self::AppState,
625 key: crossterm::event::KeyEvent,
626 ) -> bool {
627 self.tabdata().insert_key(key.code);
628
629 if self.tabdata().is_selected {
630 self.tab_keyhandler_selected(app_data, key)
631 } else {
632 self.tab_keyhandler_deselected(app_data, key)
633 }
634 }
635
636 fn widget_keyhandler(
638 &mut self,
639 app_data: &mut Self::AppState,
640 key: crossterm::event::KeyEvent,
641 area: Rect,
642 ) {
643 let cursor = self.cursor();
644 for (widget, area) in self.widgets(area) {
645 if TabData::<Self::AppState>::isitselected(area, cursor) {
646 widget.keyhandler(app_data, key);
647 return;
648 }
649 }
650 }
651
652 fn set_map(&mut self, area: Rect) {
653 let mut map = BTreeMap::new();
654 for (widget, area) in self.widgets(area) {
655 map.insert(widget.id(), area);
656 }
657 self.tabdata().area_map = map;
658 }
659
660 fn entry_render(&mut self, f: &mut Frame, app_data: &mut Self::AppState, area: Rect) {
661 self.check_popup_value(app_data);
662
663 match self.pop_up() {
664 Some(pop_up) => pop_up.entry_render(f, app_data, area),
665 None => {
666 self.set_map(area);
667 self.validate_pos(area);
668 self.pre_render_hook(app_data);
669 self.render(f, app_data, area);
670 }
671 }
672
673 self.tabdata().first_pass = true;
674 }
675
676 fn should_exit(&mut self) -> bool {
677 matches!(self.popup_state(), PopUpState::Exit)
678 }
679
680 fn cursor(&mut self) -> Pos {
681 self.tabdata().cursor
682 }
683
684 fn selected(&mut self) -> bool {
685 self.tabdata().is_selected
686 }
687
688 fn navigate(&mut self, dir: Retning) {
689 self.tabdata().navigate(dir);
690 }
691}
692
693#[derive(Default)]
694pub struct StatefulList<T> {
695 pub state: ListState,
696 pub items: Vec<T>,
697}
698
699impl<T> StatefulList<T> {
700 pub fn with_items(items: Vec<T>) -> StatefulList<T> {
701 let mut state = ListState::default();
702 if !items.is_empty() {
703 state.select(Some(0));
704 }
705 StatefulList { state, items }
706 }
707
708 pub fn next(&mut self) {
709 let i = match self.state.selected() {
710 Some(i) => {
711 if i >= self.items.len() - 1 {
712 0
713 } else {
714 i + 1
715 }
716 }
717 None => 0,
718 };
719 self.state.select(Some(i));
720 }
721
722 pub fn previous(&mut self) {
723 let i = match self.state.selected() {
724 Some(i) => {
725 if i == 0 {
726 self.items.len() - 1
727 } else {
728 i - 1
729 }
730 }
731 None => 0,
732 };
733 self.state.select(Some(i));
734 }
735
736 pub fn selected_mut(&mut self) -> Option<&mut T> {
737 match self.state.selected() {
738 Some(c) => Some(&mut self.items[c]),
739 None => None,
740 }
741 }
742
743 pub fn selected(&self) -> Option<&T> {
744 match self.state.selected() {
745 Some(c) => Some(&self.items[c]),
746 None => None,
747 }
748 }
749}
750
751impl<T: Display> Widget for StatefulList<T> {
752 type AppData = ();
753
754 fn keyhandler(&mut self, _cache: &mut (), key: crossterm::event::KeyEvent) {
755 match key.code {
756 crossterm::event::KeyCode::Up => self.previous(),
757 crossterm::event::KeyCode::Down => self.next(),
758 crossterm::event::KeyCode::Char('k') => self.previous(),
759 crossterm::event::KeyCode::Char('j') => self.next(),
760 _ => {}
761 }
762 }
763
764 fn render(&mut self, f: &mut Frame, cache: &mut (), area: Rect) {
765 let items: Vec<ListItem> = self
766 .items
767 .iter()
768 .map(|i| {
769 let i = format!("{}", i);
770 let lines = vec![Line::from(i)];
771 ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
772 })
773 .collect();
774
775 let items = List::new(items)
777 .highlight_style(
778 Style::default()
779 .bg(Color::LightGreen)
780 .add_modifier(Modifier::BOLD),
781 )
782 .highlight_symbol(">> ");
783
784 let mut state = self.state.clone();
786 f.render_stateful_widget(items, area, &mut state);
787 }
788}