1use {
2 crate::{
3 animator::Animator,
4 events::{Cancellable, EventTarget, SubscriptionHandle, SubscriptionPriority},
5 theme::Theme,
6 },
7 crossterm::{
8 ExecutableCommand,
9 event::{Event as CrosstermEvent, EventStream as CrosstermEventStream, KeyEvent, MouseEvent},
10 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
11 },
12 futures::StreamExt,
13 futures_signals::signal::Mutable,
14 ratatui::{DefaultTerminal, Frame, backend::CrosstermBackend},
15 std::{io::stdout, ops::Deref, sync::Arc},
16};
17
18pub mod animator;
19pub mod components;
20pub mod events;
21pub mod surface;
22pub mod theme;
23
24pub use surface::{
26 Cell, Position, Surface, clip, fit_height, fit_width, join_horizontal, join_vertical, place, place_filled, total_width,
27};
28
29pub trait View {
48 fn title(&self) -> &'static str { env!("CARGO_CRATE_NAME") }
50
51 fn mount(&self, _app: &EventTarget<AppEvent>) {}
55
56 fn render(&self, ctx: &mut Frame<'_>, theme: &Theme);
58
59 fn on_mouse(&self, _ev: &MouseEvent) {}
63}
64
65#[derive(Debug, Clone, Copy)]
67pub enum AppEvent {
68 Quit,
70
71 RequestAnimationFrame,
73
74 SetTheme(usize),
76
77 Resize(u16, u16),
79
80 KeyEvent(crossterm::event::KeyEvent),
82
83 MouseEvent(crossterm::event::MouseEvent),
85}
86
87struct TerminalGuard;
89
90impl Drop for TerminalGuard {
91 fn drop(&mut self) {
92 let _ = stdout().execute(crossterm::event::DisableMouseCapture);
93 let _ = stdout().execute(LeaveAlternateScreen);
94 let _ = disable_raw_mode();
95 }
96}
97
98pub struct App {
104 inner: Arc<dyn View>,
105 ev: EventTarget<AppEvent>,
106 pub theme: Mutable<Arc<Theme>>,
108 pub animator: std::sync::Mutex<Animator>,
109}
110
111impl App {
112 pub fn new(v: impl View + 'static) -> Self {
114 Self {
115 inner: Arc::new(v),
116 ev: EventTarget::new("app"),
117 theme: Mutable::new(Arc::new(Theme::default())),
118 animator: std::sync::Mutex::new(Animator::new()),
119 }
120 }
121
122 pub fn set_theme(&self, theme: Theme) { self.theme.set(Arc::new(theme)); }
124
125 pub async fn run(self) -> anyhow::Result<()> {
130 enable_raw_mode()?;
131 stdout().execute(EnterAlternateScreen)?;
132 stdout().execute(crossterm::event::EnableMouseCapture)?;
133 let _guard = TerminalGuard;
134
135 let mut dt = DefaultTerminal::new(CrosstermBackend::new(stdout()))?;
136
137 self.inner.mount(&self.ev);
138
139 let theme = self.theme.get_cloned();
141 dt.draw(|f| {
142 self.inner.render(f, &theme);
143 })?;
144
145 let mut tui = self.ev.as_stream(SubscriptionPriority::Low).fuse();
146 let mut io = CrosstermEventStream::new().fuse();
147 let mut anim_timer = tokio::time::interval(std::time::Duration::from_millis(50));
148
149 loop {
150 tokio::select! {
151 _ = anim_timer.tick() => {
152 let alive = {
153 let mut animator = self.animator.lock().unwrap();
154 animator.tick()
155 };
156 if alive {
157 let theme = self.theme.get_cloned();
158 dt.draw(|f| {
159 self.inner.render(f, &theme);
160 })?;
161 }
162 }
163
164 ev = tui.next() => {
165 if let Some(ev) = ev {
166 match *ev {
167 AppEvent::RequestAnimationFrame => {
168 let theme = self.theme.get_cloned();
169 dt.draw(|f| { self.inner.render(f, &theme); })?;
170 }
171 AppEvent::SetTheme(idx) => {
172 let preset = match idx {
173 0 => Theme::default(),
174 1 => Theme::light(),
175 2 => Theme::ocean(),
176 3 => Theme::solarized(),
177 4 => Theme::high_contrast(),
178 _ => Theme::default(),
179 };
180 self.theme.set(Arc::new(preset));
181 let theme = self.theme.get_cloned();
182 dt.draw(|f| { self.inner.render(f, &theme); })?;
183 }
184 AppEvent::Quit => break,
185 _ => {}
186 }
187 }
188 }
189
190 ev = io.next() => {
191 match ev {
192 Some(Ok(CrosstermEvent::Key(key))) => {
193 self.ev.emit(AppEvent::KeyEvent(key));
194 let theme = self.theme.get_cloned();
195 dt.draw(|f| { self.inner.render(f, &theme); })?;
196 }
197 Some(Ok(CrosstermEvent::Mouse(mouse))) => {
198 self.inner.on_mouse(&mouse);
199 self.ev.emit(AppEvent::MouseEvent(mouse));
200 let theme = self.theme.get_cloned();
201 dt.draw(|f| { self.inner.render(f, &theme); })?;
202 }
203 Some(Ok(CrosstermEvent::Resize(w, h))) => {
204 self.ev.emit(AppEvent::Resize(w, h));
205 let theme = self.theme.get_cloned();
206 dt.draw(|f| { self.inner.render(f, &theme); })?;
207 }
208 Some(Ok(_)) => {
209 let theme = self.theme.get_cloned();
210 dt.draw(|f| { self.inner.render(f, &theme); })?;
211 }
212 Some(Err(e)) => return Err(e.into()),
213 None => break,
214 }
215 }
216 }
217 }
218
219 Ok(())
220 }
221}
222
223impl Deref for App {
224 type Target = EventTarget<AppEvent>;
225
226 fn deref(&self) -> &Self::Target { &self.ev }
227}
228
229impl EventTarget<AppEvent> {
231 pub fn on_key(
236 &self,
237 priority: SubscriptionPriority,
238 handler: impl Fn(Arc<Cancellable<AppEvent>>, KeyEvent) + Send + Sync + 'static,
239 ) -> SubscriptionHandle<AppEvent> {
240 self.on(priority, move |ev| {
241 if let AppEvent::KeyEvent(key) = **ev {
242 handler(ev, key);
243 }
244 })
245 }
246}