glues_tui/
app.rs

1use {
2    crate::{context::Context, views},
3    glues_core::Glues,
4    ratatui::Frame,
5};
6
7#[cfg(not(target_arch = "wasm32"))]
8use {
9    crate::{
10        input::{Input, KeyCode, KeyEvent, KeyEventKind},
11        logger::*,
12    },
13    ratatui::DefaultTerminal,
14    std::time::Duration,
15};
16
17#[cfg(target_arch = "wasm32")]
18#[cfg(not(target_arch = "wasm32"))]
19use crate::logger::*;
20
21pub struct App {
22    pub(crate) glues: Glues,
23    pub(crate) context: Context,
24}
25
26impl Default for App {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl App {
33    pub fn new() -> Self {
34        let glues = Glues::new();
35        let context = Context::default();
36
37        Self { glues, context }
38    }
39
40    #[doc(hidden)]
41    pub fn glues_mut(&mut self) -> &mut Glues {
42        // Test-only escape hatch. Use this to simulate external backend/state
43        // mutations (e.g. another session creates directories) that cannot be
44        // reproduced through the TUI input pipeline. Production code must go
45        // through the normal event/transition flow.
46        &mut self.glues
47    }
48
49    #[cfg(not(target_arch = "wasm32"))]
50    pub async fn run(mut self, mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
51        use ratatui::crossterm as ct;
52
53        loop {
54            if self
55                .context
56                .last_log
57                .as_ref()
58                .is_some_and(|(_, created_at)| created_at.elapsed().log_unwrap().as_secs() > 5)
59            {
60                self.context.last_log = None;
61            }
62
63            terminal.draw(|frame| self.draw(frame))?;
64
65            if !ct::event::poll(Duration::from_millis(1500))? {
66                let mut transitions = Vec::new();
67                {
68                    let mut queue = self.glues.transition_queue.lock().log_unwrap();
69
70                    while let Some(transition) = queue.pop_front() {
71                        transitions.push(transition);
72                    }
73                }
74
75                for transition in transitions {
76                    self.handle_transition(transition).await;
77                }
78
79                self.save().await;
80                continue;
81            }
82
83            let raw_input = ct::event::read()?;
84            let input: Input = raw_input.into();
85
86            if !matches!(
87                input,
88                Input::Key(KeyEvent {
89                    kind: KeyEventKind::Press,
90                    ..
91                })
92            ) {
93                continue;
94            }
95
96            match input {
97                Input::Key(KeyEvent {
98                    code: KeyCode::Char('c'),
99                    modifiers,
100                    ..
101                }) if modifiers.ctrl => {
102                    self.save().await;
103                    return Ok(());
104                }
105                _ => {
106                    let action = self.context.consume(&input).await;
107                    let quit = self.handle_action(action, input).await;
108                    if quit {
109                        return Ok(());
110                    }
111                }
112            }
113        }
114    }
115
116    pub fn draw(&mut self, frame: &mut Frame) {
117        use ratatui::layout::{
118            Constraint::{Length, Percentage},
119            Layout,
120        };
121
122        let state = &self.glues.state;
123        let context = &mut self.context;
124        let vertical = Layout::vertical([Length(1), Percentage(100)]);
125        let [statusbar, body] = vertical.areas(frame.area());
126
127        views::statusbar::draw(frame, statusbar, state, &context.notebook);
128        views::body::draw(frame, body, context);
129        views::dialog::draw(frame, state, context);
130    }
131
132    pub fn context_mut(&mut self) -> &mut Context {
133        &mut self.context
134    }
135}