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 last_mouse_pos: Option<(u32, u32)> = None;
198 let mut selection = terminal::SelectionState::default();
199
200 loop {
201 let frame_start = Instant::now();
202 let (w, h) = term.size();
203 if w == 0 || h == 0 {
204 sleep_for_fps_cap(config.max_fps, frame_start);
205 continue;
206 }
207 let mut ctx = Context::new(
208 std::mem::take(&mut events),
209 w,
210 h,
211 tick,
212 focus_index,
213 prev_focus_count,
214 std::mem::take(&mut prev_scroll_infos),
215 std::mem::take(&mut prev_hit_map),
216 debug_mode,
217 config.theme,
218 last_mouse_pos,
219 );
220 ctx.process_focus_keys();
221
222 f(&mut ctx);
223
224 if ctx.should_quit {
225 break;
226 }
227
228 let mut should_copy_selection = false;
229 for ev in ctx.events.iter() {
230 if let Event::Mouse(mouse) = ev {
231 match mouse.kind {
232 event::MouseKind::Down(event::MouseButton::Left) => {
233 selection.mouse_down(mouse.x, mouse.y, &ctx.prev_hit_map);
234 }
235 event::MouseKind::Drag(event::MouseButton::Left) => {
236 selection.mouse_drag(mouse.x, mouse.y);
237 }
238 event::MouseKind::Up(event::MouseButton::Left) => {
239 should_copy_selection = selection.active;
240 }
241 _ => {}
242 }
243 }
244 }
245
246 focus_index = ctx.focus_index;
247 prev_focus_count = ctx.focus_count;
248
249 let mut tree = layout::build_tree(&ctx.commands);
250 let area = crate::rect::Rect::new(0, 0, w, h);
251 layout::compute(&mut tree, area);
252 prev_scroll_infos = layout::collect_scroll_infos(&tree);
253 prev_hit_map = layout::collect_hit_areas(&tree);
254 layout::render(&tree, term.buffer_mut());
255 if debug_mode {
256 layout::render_debug_overlay(&tree, term.buffer_mut());
257 }
258
259 if selection.active {
260 terminal::apply_selection_overlay(term.buffer_mut(), &selection);
261 }
262 if should_copy_selection {
263 let text = terminal::extract_selection_text(term.buffer_mut(), &selection);
264 if !text.is_empty() {
265 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
266 }
267 selection.clear();
268 }
269
270 term.flush()?;
271 tick = tick.wrapping_add(1);
272
273 events.clear();
274 if crossterm::event::poll(config.tick_rate)? {
275 let raw = crossterm::event::read()?;
276 if let Some(ev) = event::from_crossterm(raw) {
277 if is_ctrl_c(&ev) {
278 break;
279 }
280 if let Event::Resize(_, _) = &ev {
281 term.handle_resize()?;
282 }
283 events.push(ev);
284 }
285
286 while crossterm::event::poll(Duration::ZERO)? {
287 let raw = crossterm::event::read()?;
288 if let Some(ev) = event::from_crossterm(raw) {
289 if is_ctrl_c(&ev) {
290 return Ok(());
291 }
292 if let Event::Resize(_, _) = &ev {
293 term.handle_resize()?;
294 }
295 events.push(ev);
296 }
297 }
298
299 for ev in &events {
300 if matches!(
301 ev,
302 Event::Key(event::KeyEvent {
303 code: KeyCode::F(12),
304 ..
305 })
306 ) {
307 debug_mode = !debug_mode;
308 }
309 }
310 }
311
312 for ev in &events {
313 if let Event::Mouse(mouse) = ev {
314 last_mouse_pos = Some((mouse.x, mouse.y));
315 }
316 }
317
318 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
319 prev_hit_map.clear();
320 prev_scroll_infos.clear();
321 last_mouse_pos = None;
322 }
323
324 sleep_for_fps_cap(config.max_fps, frame_start);
325 }
326
327 Ok(())
328}
329
330#[cfg(feature = "async")]
351pub fn run_async<M: Send + 'static>(
352 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
353) -> io::Result<tokio::sync::mpsc::Sender<M>> {
354 run_async_with(RunConfig::default(), f)
355}
356
357#[cfg(feature = "async")]
364pub fn run_async_with<M: Send + 'static>(
365 config: RunConfig,
366 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
367) -> io::Result<tokio::sync::mpsc::Sender<M>> {
368 let (tx, rx) = tokio::sync::mpsc::channel(100);
369 let handle =
370 tokio::runtime::Handle::try_current().map_err(|err| io::Error::other(err.to_string()))?;
371
372 handle.spawn_blocking(move || {
373 let _ = run_async_loop(config, f, rx);
374 });
375
376 Ok(tx)
377}
378
379#[cfg(feature = "async")]
380fn run_async_loop<M: Send + 'static>(
381 config: RunConfig,
382 mut f: impl FnMut(&mut Context, &mut Vec<M>) + Send,
383 mut rx: tokio::sync::mpsc::Receiver<M>,
384) -> io::Result<()> {
385 if !io::stdout().is_terminal() {
386 return Ok(());
387 }
388
389 install_panic_hook();
390 let mut term = Terminal::new(config.mouse)?;
391 let mut events: Vec<Event> = Vec::new();
392 let mut tick: u64 = 0;
393 let mut focus_index: usize = 0;
394 let mut prev_focus_count: usize = 0;
395 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
396 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
397 let mut last_mouse_pos: Option<(u32, u32)> = None;
398 let mut selection = terminal::SelectionState::default();
399
400 loop {
401 let frame_start = Instant::now();
402 let mut messages: Vec<M> = Vec::new();
403 while let Ok(message) = rx.try_recv() {
404 messages.push(message);
405 }
406
407 let (w, h) = term.size();
408 if w == 0 || h == 0 {
409 sleep_for_fps_cap(config.max_fps, frame_start);
410 continue;
411 }
412 let mut ctx = Context::new(
413 std::mem::take(&mut events),
414 w,
415 h,
416 tick,
417 focus_index,
418 prev_focus_count,
419 std::mem::take(&mut prev_scroll_infos),
420 std::mem::take(&mut prev_hit_map),
421 false,
422 config.theme,
423 last_mouse_pos,
424 );
425 ctx.process_focus_keys();
426
427 f(&mut ctx, &mut messages);
428
429 if ctx.should_quit {
430 break;
431 }
432
433 let mut should_copy_selection = false;
434 for ev in ctx.events.iter() {
435 if let Event::Mouse(mouse) = ev {
436 match mouse.kind {
437 event::MouseKind::Down(event::MouseButton::Left) => {
438 selection.mouse_down(mouse.x, mouse.y, &ctx.prev_hit_map);
439 }
440 event::MouseKind::Drag(event::MouseButton::Left) => {
441 selection.mouse_drag(mouse.x, mouse.y);
442 }
443 event::MouseKind::Up(event::MouseButton::Left) => {
444 should_copy_selection = selection.active;
445 }
446 _ => {}
447 }
448 }
449 }
450
451 focus_index = ctx.focus_index;
452 prev_focus_count = ctx.focus_count;
453
454 let mut tree = layout::build_tree(&ctx.commands);
455 let area = crate::rect::Rect::new(0, 0, w, h);
456 layout::compute(&mut tree, area);
457 prev_scroll_infos = layout::collect_scroll_infos(&tree);
458 prev_hit_map = layout::collect_hit_areas(&tree);
459 layout::render(&tree, term.buffer_mut());
460
461 if selection.active {
462 terminal::apply_selection_overlay(term.buffer_mut(), &selection);
463 }
464 if should_copy_selection {
465 let text = terminal::extract_selection_text(term.buffer_mut(), &selection);
466 if !text.is_empty() {
467 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
468 }
469 selection.clear();
470 }
471
472 term.flush()?;
473 tick = tick.wrapping_add(1);
474
475 events.clear();
476 if crossterm::event::poll(config.tick_rate)? {
477 let raw = crossterm::event::read()?;
478 if let Some(ev) = event::from_crossterm(raw) {
479 if is_ctrl_c(&ev) {
480 break;
481 }
482 if let Event::Resize(_, _) = &ev {
483 term.handle_resize()?;
484 prev_hit_map.clear();
485 prev_scroll_infos.clear();
486 last_mouse_pos = None;
487 }
488 events.push(ev);
489 }
490
491 while crossterm::event::poll(Duration::ZERO)? {
492 let raw = crossterm::event::read()?;
493 if let Some(ev) = event::from_crossterm(raw) {
494 if is_ctrl_c(&ev) {
495 return Ok(());
496 }
497 if let Event::Resize(_, _) = &ev {
498 term.handle_resize()?;
499 prev_hit_map.clear();
500 prev_scroll_infos.clear();
501 last_mouse_pos = None;
502 }
503 events.push(ev);
504 }
505 }
506 }
507
508 for ev in &events {
509 if let Event::Mouse(mouse) = ev {
510 last_mouse_pos = Some((mouse.x, mouse.y));
511 }
512 }
513
514 sleep_for_fps_cap(config.max_fps, frame_start);
515 }
516
517 Ok(())
518}
519
520pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
536 run_inline_with(height, RunConfig::default(), f)
537}
538
539pub fn run_inline_with(
544 height: u32,
545 config: RunConfig,
546 mut f: impl FnMut(&mut Context),
547) -> io::Result<()> {
548 if !io::stdout().is_terminal() {
549 return Ok(());
550 }
551
552 install_panic_hook();
553 let mut term = InlineTerminal::new(height, config.mouse)?;
554 let mut events: Vec<Event> = Vec::new();
555 let mut debug_mode: bool = false;
556 let mut tick: u64 = 0;
557 let mut focus_index: usize = 0;
558 let mut prev_focus_count: usize = 0;
559 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
560 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
561 let mut last_mouse_pos: Option<(u32, u32)> = None;
562 let mut selection = terminal::SelectionState::default();
563
564 loop {
565 let frame_start = Instant::now();
566 let (w, h) = term.size();
567 if w == 0 || h == 0 {
568 sleep_for_fps_cap(config.max_fps, frame_start);
569 continue;
570 }
571 let mut ctx = Context::new(
572 std::mem::take(&mut events),
573 w,
574 h,
575 tick,
576 focus_index,
577 prev_focus_count,
578 std::mem::take(&mut prev_scroll_infos),
579 std::mem::take(&mut prev_hit_map),
580 debug_mode,
581 config.theme,
582 last_mouse_pos,
583 );
584 ctx.process_focus_keys();
585
586 f(&mut ctx);
587
588 if ctx.should_quit {
589 break;
590 }
591
592 let mut should_copy_selection = false;
593 for ev in ctx.events.iter() {
594 if let Event::Mouse(mouse) = ev {
595 match mouse.kind {
596 event::MouseKind::Down(event::MouseButton::Left) => {
597 selection.mouse_down(mouse.x, mouse.y, &ctx.prev_hit_map);
598 }
599 event::MouseKind::Drag(event::MouseButton::Left) => {
600 selection.mouse_drag(mouse.x, mouse.y);
601 }
602 event::MouseKind::Up(event::MouseButton::Left) => {
603 should_copy_selection = selection.active;
604 }
605 _ => {}
606 }
607 }
608 }
609
610 focus_index = ctx.focus_index;
611 prev_focus_count = ctx.focus_count;
612
613 let mut tree = layout::build_tree(&ctx.commands);
614 let area = crate::rect::Rect::new(0, 0, w, h);
615 layout::compute(&mut tree, area);
616 prev_scroll_infos = layout::collect_scroll_infos(&tree);
617 prev_hit_map = layout::collect_hit_areas(&tree);
618 layout::render(&tree, term.buffer_mut());
619 if debug_mode {
620 layout::render_debug_overlay(&tree, term.buffer_mut());
621 }
622
623 if selection.active {
624 terminal::apply_selection_overlay(term.buffer_mut(), &selection);
625 }
626 if should_copy_selection {
627 let text = terminal::extract_selection_text(term.buffer_mut(), &selection);
628 if !text.is_empty() {
629 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
630 }
631 selection.clear();
632 }
633
634 term.flush()?;
635 tick = tick.wrapping_add(1);
636
637 events.clear();
638 if crossterm::event::poll(config.tick_rate)? {
639 let raw = crossterm::event::read()?;
640 if let Some(ev) = event::from_crossterm(raw) {
641 if is_ctrl_c(&ev) {
642 break;
643 }
644 if let Event::Resize(_, _) = &ev {
645 term.handle_resize()?;
646 }
647 events.push(ev);
648 }
649
650 while crossterm::event::poll(Duration::ZERO)? {
651 let raw = crossterm::event::read()?;
652 if let Some(ev) = event::from_crossterm(raw) {
653 if is_ctrl_c(&ev) {
654 return Ok(());
655 }
656 if let Event::Resize(_, _) = &ev {
657 term.handle_resize()?;
658 }
659 events.push(ev);
660 }
661 }
662
663 for ev in &events {
664 if matches!(
665 ev,
666 Event::Key(event::KeyEvent {
667 code: KeyCode::F(12),
668 ..
669 })
670 ) {
671 debug_mode = !debug_mode;
672 }
673 }
674 }
675
676 for ev in &events {
677 if let Event::Mouse(mouse) = ev {
678 last_mouse_pos = Some((mouse.x, mouse.y));
679 }
680 }
681
682 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
683 prev_hit_map.clear();
684 prev_scroll_infos.clear();
685 last_mouse_pos = None;
686 }
687
688 sleep_for_fps_cap(config.max_fps, frame_start);
689 }
690
691 Ok(())
692}
693
694fn is_ctrl_c(ev: &Event) -> bool {
695 matches!(
696 ev,
697 Event::Key(event::KeyEvent {
698 code: KeyCode::Char('c'),
699 modifiers,
700 }) if modifiers.contains(KeyModifiers::CONTROL)
701 )
702}
703
704fn sleep_for_fps_cap(max_fps: Option<u32>, frame_start: Instant) {
705 if let Some(fps) = max_fps.filter(|fps| *fps > 0) {
706 let target = Duration::from_secs_f64(1.0 / fps as f64);
707 let elapsed = frame_start.elapsed();
708 if elapsed < target {
709 std::thread::sleep(target - elapsed);
710 }
711 }
712}