1pub mod anim;
38pub mod buffer;
39pub mod cell;
40pub mod context;
41pub mod event;
42pub mod layout;
43pub mod rect;
44pub mod style;
45mod terminal;
46pub mod widgets;
47
48use std::io;
49use std::sync::Once;
50use std::time::Duration;
51
52use event::Event;
53use terminal::{InlineTerminal, Terminal};
54
55pub use anim::{Spring, Tween};
56pub use context::{Context, Response, Widget};
57pub use event::{KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseKind};
58pub use style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
59pub use widgets::{
60 ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
61 ToastLevel, ToastMessage, ToastState,
62};
63
64static PANIC_HOOK_ONCE: Once = Once::new();
65
66fn install_panic_hook() {
67 PANIC_HOOK_ONCE.call_once(|| {
68 let original = std::panic::take_hook();
69 std::panic::set_hook(Box::new(move |panic_info| {
70 let _ = crossterm::terminal::disable_raw_mode();
71 let mut stdout = io::stdout();
72 let _ = crossterm::execute!(
73 stdout,
74 crossterm::terminal::LeaveAlternateScreen,
75 crossterm::cursor::Show,
76 crossterm::event::DisableMouseCapture,
77 crossterm::style::ResetColor,
78 crossterm::style::SetAttribute(crossterm::style::Attribute::Reset)
79 );
80 original(panic_info);
81 }));
82 });
83}
84
85pub struct RunConfig {
103 pub tick_rate: Duration,
108 pub mouse: bool,
113 pub theme: Theme,
117}
118
119impl Default for RunConfig {
120 fn default() -> Self {
121 Self {
122 tick_rate: Duration::from_millis(100),
123 mouse: false,
124 theme: Theme::dark(),
125 }
126 }
127}
128
129pub fn run(f: impl FnMut(&mut Context)) -> io::Result<()> {
144 run_with(RunConfig::default(), f)
145}
146
147pub fn run_with(config: RunConfig, mut f: impl FnMut(&mut Context)) -> io::Result<()> {
167 install_panic_hook();
168 let mut term = Terminal::new(config.mouse)?;
169 let mut events: Vec<Event> = Vec::new();
170 let mut debug_mode: bool = false;
171 let mut tick: u64 = 0;
172 let mut focus_index: usize = 0;
173 let mut prev_focus_count: usize = 0;
174 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
175 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
176 let mut last_mouse_pos: Option<(u32, u32)> = None;
177
178 loop {
179 let (w, h) = term.size();
180 if w == 0 || h == 0 {
181 continue;
182 }
183 let mut ctx = Context::new(
184 std::mem::take(&mut events),
185 w,
186 h,
187 tick,
188 focus_index,
189 prev_focus_count,
190 std::mem::take(&mut prev_scroll_infos),
191 std::mem::take(&mut prev_hit_map),
192 debug_mode,
193 config.theme,
194 last_mouse_pos,
195 );
196 ctx.process_focus_keys();
197
198 f(&mut ctx);
199
200 if ctx.should_quit {
201 break;
202 }
203
204 focus_index = ctx.focus_index;
205 prev_focus_count = ctx.focus_count;
206
207 let mut tree = layout::build_tree(&ctx.commands);
208 let area = crate::rect::Rect::new(0, 0, w, h);
209 layout::compute(&mut tree, area);
210 prev_scroll_infos = layout::collect_scroll_infos(&tree);
211 prev_hit_map = layout::collect_hit_areas(&tree);
212 layout::render(&tree, term.buffer_mut());
213 if debug_mode {
214 layout::render_debug_overlay(&tree, term.buffer_mut());
215 }
216
217 term.flush()?;
218 tick = tick.wrapping_add(1);
219
220 events.clear();
221 if crossterm::event::poll(config.tick_rate)? {
222 let raw = crossterm::event::read()?;
223 if let Some(ev) = event::from_crossterm(raw) {
224 if is_ctrl_c(&ev) {
225 break;
226 }
227 if let Event::Resize(_, _) = &ev {
228 term.handle_resize()?;
229 }
230 events.push(ev);
231 }
232
233 while crossterm::event::poll(Duration::ZERO)? {
234 let raw = crossterm::event::read()?;
235 if let Some(ev) = event::from_crossterm(raw) {
236 if is_ctrl_c(&ev) {
237 return Ok(());
238 }
239 if let Event::Resize(_, _) = &ev {
240 term.handle_resize()?;
241 }
242 events.push(ev);
243 }
244 }
245
246 for ev in &events {
247 if matches!(
248 ev,
249 Event::Key(event::KeyEvent {
250 code: KeyCode::F(12),
251 ..
252 })
253 ) {
254 debug_mode = !debug_mode;
255 }
256 }
257 }
258
259 for ev in &events {
260 if let Event::Mouse(mouse) = ev {
261 last_mouse_pos = Some((mouse.x, mouse.y));
262 }
263 }
264
265 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
266 prev_hit_map.clear();
267 prev_scroll_infos.clear();
268 last_mouse_pos = None;
269 }
270 }
271
272 Ok(())
273}
274
275#[cfg(feature = "async")]
296pub fn run_async<M: Send + 'static>(
297 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
298) -> io::Result<tokio::sync::mpsc::Sender<M>> {
299 run_async_with(RunConfig::default(), f)
300}
301
302#[cfg(feature = "async")]
309pub fn run_async_with<M: Send + 'static>(
310 config: RunConfig,
311 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
312) -> io::Result<tokio::sync::mpsc::Sender<M>> {
313 let (tx, rx) = tokio::sync::mpsc::channel(100);
314 let handle =
315 tokio::runtime::Handle::try_current().map_err(|err| io::Error::other(err.to_string()))?;
316
317 handle.spawn_blocking(move || {
318 let _ = run_async_loop(config, f, rx);
319 });
320
321 Ok(tx)
322}
323
324#[cfg(feature = "async")]
325fn run_async_loop<M: Send + 'static>(
326 config: RunConfig,
327 mut f: impl FnMut(&mut Context, &mut Vec<M>) + Send,
328 mut rx: tokio::sync::mpsc::Receiver<M>,
329) -> io::Result<()> {
330 install_panic_hook();
331 let mut term = Terminal::new(config.mouse)?;
332 let mut events: Vec<Event> = Vec::new();
333 let mut tick: u64 = 0;
334 let mut focus_index: usize = 0;
335 let mut prev_focus_count: usize = 0;
336 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
337 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
338 let mut last_mouse_pos: Option<(u32, u32)> = None;
339
340 loop {
341 let mut messages: Vec<M> = Vec::new();
342 while let Ok(message) = rx.try_recv() {
343 messages.push(message);
344 }
345
346 let (w, h) = term.size();
347 if w == 0 || h == 0 {
348 continue;
349 }
350 let mut ctx = Context::new(
351 std::mem::take(&mut events),
352 w,
353 h,
354 tick,
355 focus_index,
356 prev_focus_count,
357 std::mem::take(&mut prev_scroll_infos),
358 std::mem::take(&mut prev_hit_map),
359 false,
360 config.theme,
361 last_mouse_pos,
362 );
363 ctx.process_focus_keys();
364
365 f(&mut ctx, &mut messages);
366
367 if ctx.should_quit {
368 break;
369 }
370
371 focus_index = ctx.focus_index;
372 prev_focus_count = ctx.focus_count;
373
374 let mut tree = layout::build_tree(&ctx.commands);
375 let area = crate::rect::Rect::new(0, 0, w, h);
376 layout::compute(&mut tree, area);
377 prev_scroll_infos = layout::collect_scroll_infos(&tree);
378 prev_hit_map = layout::collect_hit_areas(&tree);
379 layout::render(&tree, term.buffer_mut());
380
381 term.flush()?;
382 tick = tick.wrapping_add(1);
383
384 events.clear();
385 if crossterm::event::poll(config.tick_rate)? {
386 let raw = crossterm::event::read()?;
387 if let Some(ev) = event::from_crossterm(raw) {
388 if is_ctrl_c(&ev) {
389 break;
390 }
391 if let Event::Resize(_, _) = &ev {
392 term.handle_resize()?;
393 prev_hit_map.clear();
394 prev_scroll_infos.clear();
395 last_mouse_pos = None;
396 }
397 events.push(ev);
398 }
399
400 while crossterm::event::poll(Duration::ZERO)? {
401 let raw = crossterm::event::read()?;
402 if let Some(ev) = event::from_crossterm(raw) {
403 if is_ctrl_c(&ev) {
404 return Ok(());
405 }
406 if let Event::Resize(_, _) = &ev {
407 term.handle_resize()?;
408 prev_hit_map.clear();
409 prev_scroll_infos.clear();
410 last_mouse_pos = None;
411 }
412 events.push(ev);
413 }
414 }
415 }
416
417 for ev in &events {
418 if let Event::Mouse(mouse) = ev {
419 last_mouse_pos = Some((mouse.x, mouse.y));
420 }
421 }
422 }
423
424 Ok(())
425}
426
427pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
443 run_inline_with(height, RunConfig::default(), f)
444}
445
446pub fn run_inline_with(
451 height: u32,
452 config: RunConfig,
453 mut f: impl FnMut(&mut Context),
454) -> io::Result<()> {
455 install_panic_hook();
456 let mut term = InlineTerminal::new(height, config.mouse)?;
457 let mut events: Vec<Event> = Vec::new();
458 let mut debug_mode: bool = false;
459 let mut tick: u64 = 0;
460 let mut focus_index: usize = 0;
461 let mut prev_focus_count: usize = 0;
462 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
463 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
464 let mut last_mouse_pos: Option<(u32, u32)> = None;
465
466 loop {
467 let (w, h) = term.size();
468 if w == 0 || h == 0 {
469 continue;
470 }
471 let mut ctx = Context::new(
472 std::mem::take(&mut events),
473 w,
474 h,
475 tick,
476 focus_index,
477 prev_focus_count,
478 std::mem::take(&mut prev_scroll_infos),
479 std::mem::take(&mut prev_hit_map),
480 debug_mode,
481 config.theme,
482 last_mouse_pos,
483 );
484 ctx.process_focus_keys();
485
486 f(&mut ctx);
487
488 if ctx.should_quit {
489 break;
490 }
491
492 focus_index = ctx.focus_index;
493 prev_focus_count = ctx.focus_count;
494
495 let mut tree = layout::build_tree(&ctx.commands);
496 let area = crate::rect::Rect::new(0, 0, w, h);
497 layout::compute(&mut tree, area);
498 prev_scroll_infos = layout::collect_scroll_infos(&tree);
499 prev_hit_map = layout::collect_hit_areas(&tree);
500 layout::render(&tree, term.buffer_mut());
501 if debug_mode {
502 layout::render_debug_overlay(&tree, term.buffer_mut());
503 }
504
505 term.flush()?;
506 tick = tick.wrapping_add(1);
507
508 events.clear();
509 if crossterm::event::poll(config.tick_rate)? {
510 let raw = crossterm::event::read()?;
511 if let Some(ev) = event::from_crossterm(raw) {
512 if is_ctrl_c(&ev) {
513 break;
514 }
515 if let Event::Resize(_, _) = &ev {
516 term.handle_resize()?;
517 }
518 events.push(ev);
519 }
520
521 while crossterm::event::poll(Duration::ZERO)? {
522 let raw = crossterm::event::read()?;
523 if let Some(ev) = event::from_crossterm(raw) {
524 if is_ctrl_c(&ev) {
525 return Ok(());
526 }
527 if let Event::Resize(_, _) = &ev {
528 term.handle_resize()?;
529 }
530 events.push(ev);
531 }
532 }
533
534 for ev in &events {
535 if matches!(
536 ev,
537 Event::Key(event::KeyEvent {
538 code: KeyCode::F(12),
539 ..
540 })
541 ) {
542 debug_mode = !debug_mode;
543 }
544 }
545 }
546
547 for ev in &events {
548 if let Event::Mouse(mouse) = ev {
549 last_mouse_pos = Some((mouse.x, mouse.y));
550 }
551 }
552
553 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
554 prev_hit_map.clear();
555 prev_scroll_infos.clear();
556 last_mouse_pos = None;
557 }
558 }
559
560 Ok(())
561}
562
563fn is_ctrl_c(ev: &Event) -> bool {
564 matches!(
565 ev,
566 Event::Key(event::KeyEvent {
567 code: KeyCode::Char('c'),
568 modifiers,
569 }) if modifiers.contains(KeyModifiers::CONTROL)
570 )
571}