ratatui_calloop/app.rs
1use std::{io, time::Duration};
2
3use crate::{
4 crossterm::CrosstermEventSource,
5 terminal::{init, restore, Terminal},
6 Result,
7};
8use calloop::{EventLoop, LoopSignal};
9use ratatui::crossterm::event::{Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent};
10
11/// A trait for the main application struct.
12///
13/// This trait defines the main interface for the application. It provides methods for drawing the
14/// application to the terminal and handling input events. Applications should implement this trait
15/// to define their behavior.
16///
17/// The `App` trait provides a default implementation for handling crossterm events. This
18/// implementation will call the appropriate event handler method based on the event type. The
19/// default implementation will ignore key release events, but this can be overridden by setting
20/// the `IGNORE_KEY_RELEASE` constant to `false`.
21///
22/// # Example
23///
24/// ```
25/// use std::io;
26/// use ratatui_calloop::{App, Terminal};
27/// use ratatui::{crossterm::event::{KeyCode, KeyEvent}, text::Text};
28///
29/// struct MyApp {
30/// should_exit: bool,
31/// counter: i32,
32/// }
33///
34/// impl App for MyApp {
35/// fn draw(&self, terminal: &mut Terminal) -> io::Result<()> {
36/// terminal.draw(|frame| {
37/// let text = Text::raw(format!("Counter: {}", self.counter));
38/// frame.render_widget(&text, frame.size());
39/// })?;
40/// Ok(())
41/// }
42///
43/// fn on_key_event(&mut self, event: KeyEvent) {
44/// match event.code {
45/// KeyCode::Char('j') => self.counter -= 1,
46/// KeyCode::Char('k') => self.counter += 1,
47/// KeyCode::Char('q') => self.should_exit = true,
48/// _ => {}
49/// }
50/// }
51/// }
52/// ```
53pub trait App {
54 const IGNORE_KEY_RELEASE: bool = true;
55
56 /// Draw the application to the terminal.
57 ///
58 /// This method should draw the application to the terminal using the provided `Terminal` instance.
59 ///
60 /// # Example
61 ///
62 /// ```
63 /// use std::io;
64 /// use ratatui_calloop::{App, Terminal};
65 /// use ratatui::text::Text;
66 ///
67 /// struct MyApp {
68 /// counter: i32,
69 /// }
70 ///
71 /// impl App for MyApp {
72 /// fn draw(&self, terminal: &mut Terminal) -> io::Result<()> {
73 /// terminal.draw(|frame| {
74 /// let text = Text::raw(format!("Counter: {}", self.counter));
75 /// frame.render_widget(&text, frame.size());
76 /// })?;
77 /// Ok(())
78 /// }
79 /// }
80 /// ```
81 fn draw(&self, terminal: &mut Terminal) -> io::Result<()>;
82
83 /// Handle a crossterm event.
84 ///
85 /// This method will be called when a crossterm event is received. The default implementation
86 /// will call the appropriate event handler method based on the event type. This can be
87 /// overridden to provide custom event handling.
88 fn on_crossterm_event(&mut self, event: CrosstermEvent) {
89 match event {
90 CrosstermEvent::FocusGained => self.on_focus_gained(),
91 CrosstermEvent::FocusLost => self.on_focus_lost(),
92 CrosstermEvent::Key(key_event) => {
93 if !Self::IGNORE_KEY_RELEASE || key_event.kind == KeyEventKind::Press {
94 self.on_key_event(key_event);
95 }
96 }
97 CrosstermEvent::Mouse(event) => self.on_mouse_event(event),
98 CrosstermEvent::Paste(text) => self.on_paste(text),
99 CrosstermEvent::Resize(width, height) => self.on_resize(width, height),
100 }
101 }
102
103 /// Handle the focus lost event.
104 ///
105 /// This method will be called when the application loses focus.
106 fn on_focus_lost(&mut self) {}
107
108 /// Handle the focus gained event.
109 ///
110 /// This method will be called when the application gains focus.
111 fn on_focus_gained(&mut self) {}
112
113 /// Handle a key event.
114 ///
115 /// This method will be called when a key event is received.
116 #[allow(unused_variables)]
117 fn on_key_event(&mut self, event: KeyEvent) {}
118
119 /// Handle a mouse event.
120 ///
121 /// This method will be called when a mouse event is received.
122 #[allow(unused_variables)]
123 fn on_mouse_event(&mut self, event: MouseEvent) {}
124
125 /// Handle a paste event.
126 ///
127 /// This method will be called when a paste event is received.
128 #[allow(unused_variables)]
129 fn on_paste(&mut self, text: String) {}
130
131 /// Handle a resize event.
132 ///
133 /// This method will be called when a resize event is received.
134 #[allow(unused_variables)]
135 fn on_resize(&mut self, width: u16, height: u16) {}
136}
137
138/// The main event loop for the application.
139///
140/// This is based on the [Calloop] crate, which provides a cross-platform event loop that can handle
141/// multiple sources of events. In this case, we're using it to handle both the terminal input and
142/// the frame timing for the TUI.
143///
144/// [Calloop]: https://docs.rs/calloop
145pub struct ApplicationLoop<T: App> {
146 event_loop: EventLoop<'static, T>,
147}
148
149impl<T: App> ApplicationLoop<T> {
150 /// Create a new `ApplicationLoop`.
151 ///
152 /// This will create a new event loop and insert a source for crossterm events.
153 pub fn new() -> Result<Self> {
154 let event_loop = EventLoop::<T>::try_new()?;
155 let crossterm_event_source = CrosstermEventSource::new()?;
156 event_loop
157 .handle()
158 .insert_source(crossterm_event_source, |event, _metadata, app| {
159 app.on_crossterm_event(event);
160 })
161 .map_err(|e| e.error)?;
162
163 Ok(Self { event_loop })
164 }
165
166 /// Run the event loop.
167 ///
168 /// This will run the event loop until the application signals that it should stop.
169 /// The application will be drawn to the terminal on each frame (60 fps).
170 pub fn run(&mut self, app: &mut T) -> Result<()> {
171 let mut terminal = init()?;
172 let frame_rate = Duration::from_secs_f32(1.0 / 2.0); // 60 fps
173 self.event_loop.run(frame_rate, app, |app| {
174 // TODO handle errors here nicely somehow rather than swallowing them
175 // likely needs to send a message or something
176 let _ = app.draw(&mut terminal);
177 })?;
178 restore()?;
179 Ok(())
180 }
181
182 /// Get the loop signal.
183 ///
184 /// This can be used to stop the event loop from outside the loop (e.g. when the application
185 /// should exit).s
186 pub fn exit_signal(&self) -> LoopSignal {
187 self.event_loop.get_signal()
188 }
189}