mischef/
lib.rs

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    /* USER DEFINED */
430
431    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    /* USER CAN CALL */
438
439    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    /// Keyhandler stuff that should run if no widgets are selected.
483    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    /// Keyhandler stuff that should run if a widget is selected.
492    /// Think of this as widget specific stuff that requires the state of the tab,
493    /// and therefore the logic cannot be handled by the widget directly.
494    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    /* INTERNAL */
525
526    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    /// its a function so that it can be overriden if needed.
550    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        // weird to do it like this but theres like double mutably borrow rules otherwise.
574        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    // Keyhandling that requires the state of the object.
620    // bool represents whether the tab 'captures' the key
621    // or passes it onto the widget in focus
622    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    // Keyhandler that only mutates the widget itself.
637    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        // Create a List from all list items and highlight the currently selected one
776        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        // We can now render the item list
785        let mut state = self.state.clone();
786        f.render_stateful_widget(items, area, &mut state);
787    }
788}