1pub mod anim;
39pub mod buffer;
40pub mod cell;
41pub mod chart;
42pub mod context;
43pub mod event;
44pub mod layout;
45pub mod rect;
46pub mod style;
47mod terminal;
48pub mod test_utils;
49pub mod widgets;
50
51use std::io;
52use std::io::IsTerminal;
53use std::sync::Once;
54use std::time::{Duration, Instant};
55
56use terminal::{InlineTerminal, Terminal};
57
58pub use crate::test_utils::{EventBuilder, TestBackend};
59pub use anim::{Spring, Tween};
60pub use chart::{
61 Axis, ChartBuilder, ChartConfig, ChartRenderer, Dataset, DatasetEntry, GraphType,
62 HistogramBuilder, LegendPosition, Marker,
63};
64pub use context::{Bar, BarDirection, BarGroup, CanvasContext, Context, Response, Widget};
65pub use event::{Event, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseKind};
66pub use style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
67pub use widgets::{
68 ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
69 ToastLevel, ToastMessage, ToastState,
70};
71
72static PANIC_HOOK_ONCE: Once = Once::new();
73
74fn install_panic_hook() {
75 PANIC_HOOK_ONCE.call_once(|| {
76 let original = std::panic::take_hook();
77 std::panic::set_hook(Box::new(move |panic_info| {
78 let _ = crossterm::terminal::disable_raw_mode();
79 let mut stdout = io::stdout();
80 let _ = crossterm::execute!(
81 stdout,
82 crossterm::terminal::LeaveAlternateScreen,
83 crossterm::cursor::Show,
84 crossterm::event::DisableMouseCapture,
85 crossterm::event::DisableBracketedPaste,
86 crossterm::style::ResetColor,
87 crossterm::style::SetAttribute(crossterm::style::Attribute::Reset)
88 );
89 original(panic_info);
90 }));
91 });
92}
93
94#[must_use = "configure loop behavior before passing to run_with or run_inline_with"]
113pub struct RunConfig {
114 pub tick_rate: Duration,
119 pub mouse: bool,
124 pub theme: Theme,
128 pub max_fps: Option<u32>,
133}
134
135impl Default for RunConfig {
136 fn default() -> Self {
137 Self {
138 tick_rate: Duration::from_millis(100),
139 mouse: false,
140 theme: Theme::dark(),
141 max_fps: None,
142 }
143 }
144}
145
146pub fn run(f: impl FnMut(&mut Context)) -> io::Result<()> {
161 run_with(RunConfig::default(), f)
162}
163
164pub fn run_with(config: RunConfig, mut f: impl FnMut(&mut Context)) -> io::Result<()> {
184 if !io::stdout().is_terminal() {
185 return Ok(());
186 }
187
188 install_panic_hook();
189 let mut term = Terminal::new(config.mouse)?;
190 let mut events: Vec<Event> = Vec::new();
191 let mut debug_mode: bool = false;
192 let mut tick: u64 = 0;
193 let mut focus_index: usize = 0;
194 let mut prev_focus_count: usize = 0;
195 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
196 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
197 let mut prev_content_map: Vec<(rect::Rect, rect::Rect)> = Vec::new();
198 let mut prev_focus_rects: Vec<(usize, rect::Rect)> = Vec::new();
199 let mut last_mouse_pos: Option<(u32, u32)> = None;
200 let mut selection = terminal::SelectionState::default();
201
202 loop {
203 let frame_start = Instant::now();
204 let (w, h) = term.size();
205 if w == 0 || h == 0 {
206 sleep_for_fps_cap(config.max_fps, frame_start);
207 continue;
208 }
209 let mut ctx = Context::new(
210 std::mem::take(&mut events),
211 w,
212 h,
213 tick,
214 focus_index,
215 prev_focus_count,
216 std::mem::take(&mut prev_scroll_infos),
217 std::mem::take(&mut prev_hit_map),
218 std::mem::take(&mut prev_focus_rects),
219 debug_mode,
220 config.theme,
221 last_mouse_pos,
222 );
223 ctx.process_focus_keys();
224
225 f(&mut ctx);
226
227 if ctx.should_quit {
228 break;
229 }
230
231 let mut should_copy_selection = false;
232 for ev in ctx.events.iter() {
233 if let Event::Mouse(mouse) = ev {
234 match mouse.kind {
235 event::MouseKind::Down(event::MouseButton::Left) => {
236 selection.mouse_down(mouse.x, mouse.y, &prev_content_map);
237 }
238 event::MouseKind::Drag(event::MouseButton::Left) => {
239 selection.mouse_drag(mouse.x, mouse.y, &prev_content_map);
240 }
241 event::MouseKind::Up(event::MouseButton::Left) => {
242 should_copy_selection = selection.active;
243 }
244 _ => {}
245 }
246 }
247 }
248
249 focus_index = ctx.focus_index;
250 prev_focus_count = ctx.focus_count;
251
252 let mut tree = layout::build_tree(&ctx.commands);
253 let area = crate::rect::Rect::new(0, 0, w, h);
254 layout::compute(&mut tree, area);
255 prev_scroll_infos = layout::collect_scroll_infos(&tree);
256 prev_hit_map = layout::collect_hit_areas(&tree);
257 prev_content_map = layout::collect_content_areas(&tree);
258 prev_focus_rects = layout::collect_focus_rects(&tree);
259 layout::render(&tree, term.buffer_mut());
260 if debug_mode {
261 layout::render_debug_overlay(&tree, term.buffer_mut());
262 }
263
264 if selection.active {
265 terminal::apply_selection_overlay(term.buffer_mut(), &selection, &prev_content_map);
266 }
267 if should_copy_selection {
268 let text =
269 terminal::extract_selection_text(term.buffer_mut(), &selection, &prev_content_map);
270 if !text.is_empty() {
271 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
272 }
273 selection.clear();
274 }
275
276 term.flush()?;
277 tick = tick.wrapping_add(1);
278
279 events.clear();
280 if crossterm::event::poll(config.tick_rate)? {
281 let raw = crossterm::event::read()?;
282 if let Some(ev) = event::from_crossterm(raw) {
283 if is_ctrl_c(&ev) {
284 break;
285 }
286 if let Event::Resize(_, _) = &ev {
287 term.handle_resize()?;
288 }
289 events.push(ev);
290 }
291
292 while crossterm::event::poll(Duration::ZERO)? {
293 let raw = crossterm::event::read()?;
294 if let Some(ev) = event::from_crossterm(raw) {
295 if is_ctrl_c(&ev) {
296 return Ok(());
297 }
298 if let Event::Resize(_, _) = &ev {
299 term.handle_resize()?;
300 }
301 events.push(ev);
302 }
303 }
304
305 for ev in &events {
306 if matches!(
307 ev,
308 Event::Key(event::KeyEvent {
309 code: KeyCode::F(12),
310 ..
311 })
312 ) {
313 debug_mode = !debug_mode;
314 }
315 }
316 }
317
318 for ev in &events {
319 if let Event::Mouse(mouse) = ev {
320 last_mouse_pos = Some((mouse.x, mouse.y));
321 }
322 }
323
324 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
325 prev_hit_map.clear();
326 prev_content_map.clear();
327 prev_focus_rects.clear();
328 prev_scroll_infos.clear();
329 last_mouse_pos = None;
330 }
331
332 sleep_for_fps_cap(config.max_fps, frame_start);
333 }
334
335 Ok(())
336}
337
338#[cfg(feature = "async")]
359pub fn run_async<M: Send + 'static>(
360 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
361) -> io::Result<tokio::sync::mpsc::Sender<M>> {
362 run_async_with(RunConfig::default(), f)
363}
364
365#[cfg(feature = "async")]
372pub fn run_async_with<M: Send + 'static>(
373 config: RunConfig,
374 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
375) -> io::Result<tokio::sync::mpsc::Sender<M>> {
376 let (tx, rx) = tokio::sync::mpsc::channel(100);
377 let handle =
378 tokio::runtime::Handle::try_current().map_err(|err| io::Error::other(err.to_string()))?;
379
380 handle.spawn_blocking(move || {
381 let _ = run_async_loop(config, f, rx);
382 });
383
384 Ok(tx)
385}
386
387#[cfg(feature = "async")]
388fn run_async_loop<M: Send + 'static>(
389 config: RunConfig,
390 mut f: impl FnMut(&mut Context, &mut Vec<M>) + Send,
391 mut rx: tokio::sync::mpsc::Receiver<M>,
392) -> io::Result<()> {
393 if !io::stdout().is_terminal() {
394 return Ok(());
395 }
396
397 install_panic_hook();
398 let mut term = Terminal::new(config.mouse)?;
399 let mut events: Vec<Event> = Vec::new();
400 let mut tick: u64 = 0;
401 let mut focus_index: usize = 0;
402 let mut prev_focus_count: usize = 0;
403 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
404 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
405 let mut prev_content_map: Vec<(rect::Rect, rect::Rect)> = Vec::new();
406 let mut prev_focus_rects: Vec<(usize, rect::Rect)> = Vec::new();
407 let mut last_mouse_pos: Option<(u32, u32)> = None;
408 let mut selection = terminal::SelectionState::default();
409
410 loop {
411 let frame_start = Instant::now();
412 let mut messages: Vec<M> = Vec::new();
413 while let Ok(message) = rx.try_recv() {
414 messages.push(message);
415 }
416
417 let (w, h) = term.size();
418 if w == 0 || h == 0 {
419 sleep_for_fps_cap(config.max_fps, frame_start);
420 continue;
421 }
422 let mut ctx = Context::new(
423 std::mem::take(&mut events),
424 w,
425 h,
426 tick,
427 focus_index,
428 prev_focus_count,
429 std::mem::take(&mut prev_scroll_infos),
430 std::mem::take(&mut prev_hit_map),
431 std::mem::take(&mut prev_focus_rects),
432 false,
433 config.theme,
434 last_mouse_pos,
435 );
436 ctx.process_focus_keys();
437
438 f(&mut ctx, &mut messages);
439
440 if ctx.should_quit {
441 break;
442 }
443
444 let mut should_copy_selection = false;
445 for ev in ctx.events.iter() {
446 if let Event::Mouse(mouse) = ev {
447 match mouse.kind {
448 event::MouseKind::Down(event::MouseButton::Left) => {
449 selection.mouse_down(mouse.x, mouse.y, &prev_content_map);
450 }
451 event::MouseKind::Drag(event::MouseButton::Left) => {
452 selection.mouse_drag(mouse.x, mouse.y, &prev_content_map);
453 }
454 event::MouseKind::Up(event::MouseButton::Left) => {
455 should_copy_selection = selection.active;
456 }
457 _ => {}
458 }
459 }
460 }
461
462 focus_index = ctx.focus_index;
463 prev_focus_count = ctx.focus_count;
464
465 let mut tree = layout::build_tree(&ctx.commands);
466 let area = crate::rect::Rect::new(0, 0, w, h);
467 layout::compute(&mut tree, area);
468 prev_scroll_infos = layout::collect_scroll_infos(&tree);
469 prev_hit_map = layout::collect_hit_areas(&tree);
470 prev_content_map = layout::collect_content_areas(&tree);
471 prev_focus_rects = layout::collect_focus_rects(&tree);
472 layout::render(&tree, term.buffer_mut());
473
474 if selection.active {
475 terminal::apply_selection_overlay(term.buffer_mut(), &selection, &prev_content_map);
476 }
477 if should_copy_selection {
478 let text =
479 terminal::extract_selection_text(term.buffer_mut(), &selection, &prev_content_map);
480 if !text.is_empty() {
481 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
482 }
483 selection.clear();
484 }
485
486 term.flush()?;
487 tick = tick.wrapping_add(1);
488
489 events.clear();
490 if crossterm::event::poll(config.tick_rate)? {
491 let raw = crossterm::event::read()?;
492 if let Some(ev) = event::from_crossterm(raw) {
493 if is_ctrl_c(&ev) {
494 break;
495 }
496 if let Event::Resize(_, _) = &ev {
497 term.handle_resize()?;
498 prev_hit_map.clear();
499 prev_content_map.clear();
500 prev_focus_rects.clear();
501 prev_scroll_infos.clear();
502 last_mouse_pos = None;
503 }
504 events.push(ev);
505 }
506
507 while crossterm::event::poll(Duration::ZERO)? {
508 let raw = crossterm::event::read()?;
509 if let Some(ev) = event::from_crossterm(raw) {
510 if is_ctrl_c(&ev) {
511 return Ok(());
512 }
513 if let Event::Resize(_, _) = &ev {
514 term.handle_resize()?;
515 prev_hit_map.clear();
516 prev_content_map.clear();
517 prev_focus_rects.clear();
518 prev_scroll_infos.clear();
519 last_mouse_pos = None;
520 }
521 events.push(ev);
522 }
523 }
524 }
525
526 for ev in &events {
527 if let Event::Mouse(mouse) = ev {
528 last_mouse_pos = Some((mouse.x, mouse.y));
529 }
530 }
531
532 sleep_for_fps_cap(config.max_fps, frame_start);
533 }
534
535 Ok(())
536}
537
538pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
554 run_inline_with(height, RunConfig::default(), f)
555}
556
557pub fn run_inline_with(
562 height: u32,
563 config: RunConfig,
564 mut f: impl FnMut(&mut Context),
565) -> io::Result<()> {
566 if !io::stdout().is_terminal() {
567 return Ok(());
568 }
569
570 install_panic_hook();
571 let mut term = InlineTerminal::new(height, config.mouse)?;
572 let mut events: Vec<Event> = Vec::new();
573 let mut debug_mode: bool = false;
574 let mut tick: u64 = 0;
575 let mut focus_index: usize = 0;
576 let mut prev_focus_count: usize = 0;
577 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
578 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
579 let mut prev_content_map: Vec<(rect::Rect, rect::Rect)> = Vec::new();
580 let mut prev_focus_rects: Vec<(usize, rect::Rect)> = Vec::new();
581 let mut last_mouse_pos: Option<(u32, u32)> = None;
582 let mut selection = terminal::SelectionState::default();
583
584 loop {
585 let frame_start = Instant::now();
586 let (w, h) = term.size();
587 if w == 0 || h == 0 {
588 sleep_for_fps_cap(config.max_fps, frame_start);
589 continue;
590 }
591 let mut ctx = Context::new(
592 std::mem::take(&mut events),
593 w,
594 h,
595 tick,
596 focus_index,
597 prev_focus_count,
598 std::mem::take(&mut prev_scroll_infos),
599 std::mem::take(&mut prev_hit_map),
600 std::mem::take(&mut prev_focus_rects),
601 debug_mode,
602 config.theme,
603 last_mouse_pos,
604 );
605 ctx.process_focus_keys();
606
607 f(&mut ctx);
608
609 if ctx.should_quit {
610 break;
611 }
612
613 let mut should_copy_selection = false;
614 for ev in ctx.events.iter() {
615 if let Event::Mouse(mouse) = ev {
616 match mouse.kind {
617 event::MouseKind::Down(event::MouseButton::Left) => {
618 selection.mouse_down(mouse.x, mouse.y, &prev_content_map);
619 }
620 event::MouseKind::Drag(event::MouseButton::Left) => {
621 selection.mouse_drag(mouse.x, mouse.y, &prev_content_map);
622 }
623 event::MouseKind::Up(event::MouseButton::Left) => {
624 should_copy_selection = selection.active;
625 }
626 _ => {}
627 }
628 }
629 }
630
631 focus_index = ctx.focus_index;
632 prev_focus_count = ctx.focus_count;
633
634 let mut tree = layout::build_tree(&ctx.commands);
635 let area = crate::rect::Rect::new(0, 0, w, h);
636 layout::compute(&mut tree, area);
637 prev_scroll_infos = layout::collect_scroll_infos(&tree);
638 prev_hit_map = layout::collect_hit_areas(&tree);
639 prev_content_map = layout::collect_content_areas(&tree);
640 prev_focus_rects = layout::collect_focus_rects(&tree);
641 layout::render(&tree, term.buffer_mut());
642 if debug_mode {
643 layout::render_debug_overlay(&tree, term.buffer_mut());
644 }
645
646 if selection.active {
647 terminal::apply_selection_overlay(term.buffer_mut(), &selection, &prev_content_map);
648 }
649 if should_copy_selection {
650 let text =
651 terminal::extract_selection_text(term.buffer_mut(), &selection, &prev_content_map);
652 if !text.is_empty() {
653 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
654 }
655 selection.clear();
656 }
657
658 term.flush()?;
659 tick = tick.wrapping_add(1);
660
661 events.clear();
662 if crossterm::event::poll(config.tick_rate)? {
663 let raw = crossterm::event::read()?;
664 if let Some(ev) = event::from_crossterm(raw) {
665 if is_ctrl_c(&ev) {
666 break;
667 }
668 if let Event::Resize(_, _) = &ev {
669 term.handle_resize()?;
670 }
671 events.push(ev);
672 }
673
674 while crossterm::event::poll(Duration::ZERO)? {
675 let raw = crossterm::event::read()?;
676 if let Some(ev) = event::from_crossterm(raw) {
677 if is_ctrl_c(&ev) {
678 return Ok(());
679 }
680 if let Event::Resize(_, _) = &ev {
681 term.handle_resize()?;
682 }
683 events.push(ev);
684 }
685 }
686
687 for ev in &events {
688 if matches!(
689 ev,
690 Event::Key(event::KeyEvent {
691 code: KeyCode::F(12),
692 ..
693 })
694 ) {
695 debug_mode = !debug_mode;
696 }
697 }
698 }
699
700 for ev in &events {
701 if let Event::Mouse(mouse) = ev {
702 last_mouse_pos = Some((mouse.x, mouse.y));
703 }
704 }
705
706 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
707 prev_hit_map.clear();
708 prev_content_map.clear();
709 prev_focus_rects.clear();
710 prev_scroll_infos.clear();
711 last_mouse_pos = None;
712 }
713
714 sleep_for_fps_cap(config.max_fps, frame_start);
715 }
716
717 Ok(())
718}
719
720fn is_ctrl_c(ev: &Event) -> bool {
721 matches!(
722 ev,
723 Event::Key(event::KeyEvent {
724 code: KeyCode::Char('c'),
725 modifiers,
726 }) if modifiers.contains(KeyModifiers::CONTROL)
727 )
728}
729
730fn sleep_for_fps_cap(max_fps: Option<u32>, frame_start: Instant) {
731 if let Some(fps) = max_fps.filter(|fps| *fps > 0) {
732 let target = Duration::from_secs_f64(1.0 / fps as f64);
733 let elapsed = frame_start.elapsed();
734 if elapsed < target {
735 std::thread::sleep(target - elapsed);
736 }
737 }
738}