1pub mod anim;
39pub mod buffer;
40pub mod cell;
41pub mod context;
42pub mod event;
43pub mod layout;
44pub mod rect;
45pub mod style;
46mod terminal;
47pub mod test_utils;
48pub mod widgets;
49
50use std::io;
51use std::io::IsTerminal;
52use std::sync::Once;
53use std::time::{Duration, Instant};
54
55use event::Event;
56use terminal::{InlineTerminal, Terminal};
57
58pub use crate::test_utils::{EventBuilder, TestBackend};
59pub use anim::{Spring, Tween};
60pub use context::{CanvasContext, Context, Response, Widget};
61pub use event::{KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseKind};
62pub use style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
63pub use widgets::{
64 ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
65 ToastLevel, ToastMessage, ToastState,
66};
67
68static PANIC_HOOK_ONCE: Once = Once::new();
69
70fn install_panic_hook() {
71 PANIC_HOOK_ONCE.call_once(|| {
72 let original = std::panic::take_hook();
73 std::panic::set_hook(Box::new(move |panic_info| {
74 let _ = crossterm::terminal::disable_raw_mode();
75 let mut stdout = io::stdout();
76 let _ = crossterm::execute!(
77 stdout,
78 crossterm::terminal::LeaveAlternateScreen,
79 crossterm::cursor::Show,
80 crossterm::event::DisableMouseCapture,
81 crossterm::style::ResetColor,
82 crossterm::style::SetAttribute(crossterm::style::Attribute::Reset)
83 );
84 original(panic_info);
85 }));
86 });
87}
88
89#[must_use = "configure loop behavior before passing to run_with or run_inline_with"]
108pub struct RunConfig {
109 pub tick_rate: Duration,
114 pub mouse: bool,
119 pub theme: Theme,
123 pub max_fps: Option<u32>,
128}
129
130impl Default for RunConfig {
131 fn default() -> Self {
132 Self {
133 tick_rate: Duration::from_millis(100),
134 mouse: false,
135 theme: Theme::dark(),
136 max_fps: None,
137 }
138 }
139}
140
141pub fn run(f: impl FnMut(&mut Context)) -> io::Result<()> {
156 run_with(RunConfig::default(), f)
157}
158
159pub fn run_with(config: RunConfig, mut f: impl FnMut(&mut Context)) -> io::Result<()> {
179 if !io::stdout().is_terminal() {
180 return Ok(());
181 }
182
183 install_panic_hook();
184 let mut term = Terminal::new(config.mouse)?;
185 let mut events: Vec<Event> = Vec::new();
186 let mut debug_mode: bool = false;
187 let mut tick: u64 = 0;
188 let mut focus_index: usize = 0;
189 let mut prev_focus_count: usize = 0;
190 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
191 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
192 let mut last_mouse_pos: Option<(u32, u32)> = None;
193
194 loop {
195 let frame_start = Instant::now();
196 let (w, h) = term.size();
197 if w == 0 || h == 0 {
198 sleep_for_fps_cap(config.max_fps, frame_start);
199 continue;
200 }
201 let mut ctx = Context::new(
202 std::mem::take(&mut events),
203 w,
204 h,
205 tick,
206 focus_index,
207 prev_focus_count,
208 std::mem::take(&mut prev_scroll_infos),
209 std::mem::take(&mut prev_hit_map),
210 debug_mode,
211 config.theme,
212 last_mouse_pos,
213 );
214 ctx.process_focus_keys();
215
216 f(&mut ctx);
217
218 if ctx.should_quit {
219 break;
220 }
221
222 focus_index = ctx.focus_index;
223 prev_focus_count = ctx.focus_count;
224
225 let mut tree = layout::build_tree(&ctx.commands);
226 let area = crate::rect::Rect::new(0, 0, w, h);
227 layout::compute(&mut tree, area);
228 prev_scroll_infos = layout::collect_scroll_infos(&tree);
229 prev_hit_map = layout::collect_hit_areas(&tree);
230 layout::render(&tree, term.buffer_mut());
231 if debug_mode {
232 layout::render_debug_overlay(&tree, term.buffer_mut());
233 }
234
235 term.flush()?;
236 tick = tick.wrapping_add(1);
237
238 events.clear();
239 if crossterm::event::poll(config.tick_rate)? {
240 let raw = crossterm::event::read()?;
241 if let Some(ev) = event::from_crossterm(raw) {
242 if is_ctrl_c(&ev) {
243 break;
244 }
245 if let Event::Resize(_, _) = &ev {
246 term.handle_resize()?;
247 }
248 events.push(ev);
249 }
250
251 while crossterm::event::poll(Duration::ZERO)? {
252 let raw = crossterm::event::read()?;
253 if let Some(ev) = event::from_crossterm(raw) {
254 if is_ctrl_c(&ev) {
255 return Ok(());
256 }
257 if let Event::Resize(_, _) = &ev {
258 term.handle_resize()?;
259 }
260 events.push(ev);
261 }
262 }
263
264 for ev in &events {
265 if matches!(
266 ev,
267 Event::Key(event::KeyEvent {
268 code: KeyCode::F(12),
269 ..
270 })
271 ) {
272 debug_mode = !debug_mode;
273 }
274 }
275 }
276
277 for ev in &events {
278 if let Event::Mouse(mouse) = ev {
279 last_mouse_pos = Some((mouse.x, mouse.y));
280 }
281 }
282
283 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
284 prev_hit_map.clear();
285 prev_scroll_infos.clear();
286 last_mouse_pos = None;
287 }
288
289 sleep_for_fps_cap(config.max_fps, frame_start);
290 }
291
292 Ok(())
293}
294
295#[cfg(feature = "async")]
316pub fn run_async<M: Send + 'static>(
317 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
318) -> io::Result<tokio::sync::mpsc::Sender<M>> {
319 run_async_with(RunConfig::default(), f)
320}
321
322#[cfg(feature = "async")]
329pub fn run_async_with<M: Send + 'static>(
330 config: RunConfig,
331 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
332) -> io::Result<tokio::sync::mpsc::Sender<M>> {
333 let (tx, rx) = tokio::sync::mpsc::channel(100);
334 let handle =
335 tokio::runtime::Handle::try_current().map_err(|err| io::Error::other(err.to_string()))?;
336
337 handle.spawn_blocking(move || {
338 let _ = run_async_loop(config, f, rx);
339 });
340
341 Ok(tx)
342}
343
344#[cfg(feature = "async")]
345fn run_async_loop<M: Send + 'static>(
346 config: RunConfig,
347 mut f: impl FnMut(&mut Context, &mut Vec<M>) + Send,
348 mut rx: tokio::sync::mpsc::Receiver<M>,
349) -> io::Result<()> {
350 if !io::stdout().is_terminal() {
351 return Ok(());
352 }
353
354 install_panic_hook();
355 let mut term = Terminal::new(config.mouse)?;
356 let mut events: Vec<Event> = Vec::new();
357 let mut tick: u64 = 0;
358 let mut focus_index: usize = 0;
359 let mut prev_focus_count: usize = 0;
360 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
361 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
362 let mut last_mouse_pos: Option<(u32, u32)> = None;
363
364 loop {
365 let frame_start = Instant::now();
366 let mut messages: Vec<M> = Vec::new();
367 while let Ok(message) = rx.try_recv() {
368 messages.push(message);
369 }
370
371 let (w, h) = term.size();
372 if w == 0 || h == 0 {
373 sleep_for_fps_cap(config.max_fps, frame_start);
374 continue;
375 }
376 let mut ctx = Context::new(
377 std::mem::take(&mut events),
378 w,
379 h,
380 tick,
381 focus_index,
382 prev_focus_count,
383 std::mem::take(&mut prev_scroll_infos),
384 std::mem::take(&mut prev_hit_map),
385 false,
386 config.theme,
387 last_mouse_pos,
388 );
389 ctx.process_focus_keys();
390
391 f(&mut ctx, &mut messages);
392
393 if ctx.should_quit {
394 break;
395 }
396
397 focus_index = ctx.focus_index;
398 prev_focus_count = ctx.focus_count;
399
400 let mut tree = layout::build_tree(&ctx.commands);
401 let area = crate::rect::Rect::new(0, 0, w, h);
402 layout::compute(&mut tree, area);
403 prev_scroll_infos = layout::collect_scroll_infos(&tree);
404 prev_hit_map = layout::collect_hit_areas(&tree);
405 layout::render(&tree, term.buffer_mut());
406
407 term.flush()?;
408 tick = tick.wrapping_add(1);
409
410 events.clear();
411 if crossterm::event::poll(config.tick_rate)? {
412 let raw = crossterm::event::read()?;
413 if let Some(ev) = event::from_crossterm(raw) {
414 if is_ctrl_c(&ev) {
415 break;
416 }
417 if let Event::Resize(_, _) = &ev {
418 term.handle_resize()?;
419 prev_hit_map.clear();
420 prev_scroll_infos.clear();
421 last_mouse_pos = None;
422 }
423 events.push(ev);
424 }
425
426 while crossterm::event::poll(Duration::ZERO)? {
427 let raw = crossterm::event::read()?;
428 if let Some(ev) = event::from_crossterm(raw) {
429 if is_ctrl_c(&ev) {
430 return Ok(());
431 }
432 if let Event::Resize(_, _) = &ev {
433 term.handle_resize()?;
434 prev_hit_map.clear();
435 prev_scroll_infos.clear();
436 last_mouse_pos = None;
437 }
438 events.push(ev);
439 }
440 }
441 }
442
443 for ev in &events {
444 if let Event::Mouse(mouse) = ev {
445 last_mouse_pos = Some((mouse.x, mouse.y));
446 }
447 }
448
449 sleep_for_fps_cap(config.max_fps, frame_start);
450 }
451
452 Ok(())
453}
454
455pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
471 run_inline_with(height, RunConfig::default(), f)
472}
473
474pub fn run_inline_with(
479 height: u32,
480 config: RunConfig,
481 mut f: impl FnMut(&mut Context),
482) -> io::Result<()> {
483 if !io::stdout().is_terminal() {
484 return Ok(());
485 }
486
487 install_panic_hook();
488 let mut term = InlineTerminal::new(height, config.mouse)?;
489 let mut events: Vec<Event> = Vec::new();
490 let mut debug_mode: bool = false;
491 let mut tick: u64 = 0;
492 let mut focus_index: usize = 0;
493 let mut prev_focus_count: usize = 0;
494 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
495 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
496 let mut last_mouse_pos: Option<(u32, u32)> = None;
497
498 loop {
499 let frame_start = Instant::now();
500 let (w, h) = term.size();
501 if w == 0 || h == 0 {
502 sleep_for_fps_cap(config.max_fps, frame_start);
503 continue;
504 }
505 let mut ctx = Context::new(
506 std::mem::take(&mut events),
507 w,
508 h,
509 tick,
510 focus_index,
511 prev_focus_count,
512 std::mem::take(&mut prev_scroll_infos),
513 std::mem::take(&mut prev_hit_map),
514 debug_mode,
515 config.theme,
516 last_mouse_pos,
517 );
518 ctx.process_focus_keys();
519
520 f(&mut ctx);
521
522 if ctx.should_quit {
523 break;
524 }
525
526 focus_index = ctx.focus_index;
527 prev_focus_count = ctx.focus_count;
528
529 let mut tree = layout::build_tree(&ctx.commands);
530 let area = crate::rect::Rect::new(0, 0, w, h);
531 layout::compute(&mut tree, area);
532 prev_scroll_infos = layout::collect_scroll_infos(&tree);
533 prev_hit_map = layout::collect_hit_areas(&tree);
534 layout::render(&tree, term.buffer_mut());
535 if debug_mode {
536 layout::render_debug_overlay(&tree, term.buffer_mut());
537 }
538
539 term.flush()?;
540 tick = tick.wrapping_add(1);
541
542 events.clear();
543 if crossterm::event::poll(config.tick_rate)? {
544 let raw = crossterm::event::read()?;
545 if let Some(ev) = event::from_crossterm(raw) {
546 if is_ctrl_c(&ev) {
547 break;
548 }
549 if let Event::Resize(_, _) = &ev {
550 term.handle_resize()?;
551 }
552 events.push(ev);
553 }
554
555 while crossterm::event::poll(Duration::ZERO)? {
556 let raw = crossterm::event::read()?;
557 if let Some(ev) = event::from_crossterm(raw) {
558 if is_ctrl_c(&ev) {
559 return Ok(());
560 }
561 if let Event::Resize(_, _) = &ev {
562 term.handle_resize()?;
563 }
564 events.push(ev);
565 }
566 }
567
568 for ev in &events {
569 if matches!(
570 ev,
571 Event::Key(event::KeyEvent {
572 code: KeyCode::F(12),
573 ..
574 })
575 ) {
576 debug_mode = !debug_mode;
577 }
578 }
579 }
580
581 for ev in &events {
582 if let Event::Mouse(mouse) = ev {
583 last_mouse_pos = Some((mouse.x, mouse.y));
584 }
585 }
586
587 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
588 prev_hit_map.clear();
589 prev_scroll_infos.clear();
590 last_mouse_pos = None;
591 }
592
593 sleep_for_fps_cap(config.max_fps, frame_start);
594 }
595
596 Ok(())
597}
598
599fn is_ctrl_c(ev: &Event) -> bool {
600 matches!(
601 ev,
602 Event::Key(event::KeyEvent {
603 code: KeyCode::Char('c'),
604 modifiers,
605 }) if modifiers.contains(KeyModifiers::CONTROL)
606 )
607}
608
609fn sleep_for_fps_cap(max_fps: Option<u32>, frame_start: Instant) {
610 if let Some(fps) = max_fps.filter(|fps| *fps > 0) {
611 let target = Duration::from_secs_f64(1.0 / fps as f64);
612 let elapsed = frame_start.elapsed();
613 if elapsed < target {
614 std::thread::sleep(target - elapsed);
615 }
616 }
617}