1pub mod anim;
39pub mod buffer;
40pub mod cell;
41pub mod chart;
42pub mod context;
43pub mod event;
44pub mod halfblock;
45pub mod layout;
46pub mod rect;
47pub mod style;
48mod terminal;
49pub mod test_utils;
50pub mod widgets;
51
52use std::io;
53use std::io::IsTerminal;
54use std::sync::Once;
55use std::time::{Duration, Instant};
56
57use terminal::{InlineTerminal, Terminal};
58
59pub use crate::test_utils::{EventBuilder, TestBackend};
60pub use anim::{Keyframes, LoopMode, Sequence, Spring, Stagger, Tween};
61pub use chart::{
62 Axis, ChartBuilder, ChartConfig, ChartRenderer, Dataset, DatasetEntry, GraphType,
63 HistogramBuilder, LegendPosition, Marker,
64};
65pub use context::{Bar, BarDirection, BarGroup, CanvasContext, Context, Response, State, Widget};
66pub use event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseKind};
67pub use halfblock::HalfBlockImage;
68pub use style::{
69 Align, Border, BorderSides, Breakpoint, Color, ColorDepth, Constraints, Justify, Margin,
70 Modifiers, Padding, Style, Theme, ThemeBuilder,
71};
72pub use widgets::{
73 ApprovalAction, ButtonVariant, CommandPaletteState, ContextItem, FormField, FormState,
74 ListState, MultiSelectState, PaletteCommand, RadioState, ScrollState, SelectState,
75 SpinnerState, StreamingTextState, TableState, TabsState, TextInputState, TextareaState,
76 ToastLevel, ToastMessage, ToastState, ToolApprovalState, TreeNode, TreeState,
77};
78
79static PANIC_HOOK_ONCE: Once = Once::new();
80
81fn install_panic_hook() {
82 PANIC_HOOK_ONCE.call_once(|| {
83 let original = std::panic::take_hook();
84 std::panic::set_hook(Box::new(move |panic_info| {
85 let _ = crossterm::terminal::disable_raw_mode();
86 let mut stdout = io::stdout();
87 let _ = crossterm::execute!(
88 stdout,
89 crossterm::terminal::LeaveAlternateScreen,
90 crossterm::cursor::Show,
91 crossterm::event::DisableMouseCapture,
92 crossterm::event::DisableBracketedPaste,
93 crossterm::style::ResetColor,
94 crossterm::style::SetAttribute(crossterm::style::Attribute::Reset)
95 );
96
97 eprintln!("\n\x1b[1;31m━━━ SLT Panic ━━━\x1b[0m\n");
99
100 if let Some(location) = panic_info.location() {
102 eprintln!(
103 "\x1b[90m{}:{}:{}\x1b[0m",
104 location.file(),
105 location.line(),
106 location.column()
107 );
108 }
109
110 if let Some(msg) = panic_info.payload().downcast_ref::<&str>() {
112 eprintln!("\x1b[1m{}\x1b[0m", msg);
113 } else if let Some(msg) = panic_info.payload().downcast_ref::<String>() {
114 eprintln!("\x1b[1m{}\x1b[0m", msg);
115 }
116
117 eprintln!(
118 "\n\x1b[90mTerminal state restored. Report bugs at https://github.com/subinium/SuperLightTUI/issues\x1b[0m\n"
119 );
120
121 original(panic_info);
122 }));
123 });
124}
125
126#[must_use = "configure loop behavior before passing to run_with or run_inline_with"]
147pub struct RunConfig {
148 pub tick_rate: Duration,
153 pub mouse: bool,
158 pub kitty_keyboard: bool,
165 pub theme: Theme,
169 pub color_depth: Option<ColorDepth>,
175 pub max_fps: Option<u32>,
180}
181
182impl Default for RunConfig {
183 fn default() -> Self {
184 Self {
185 tick_rate: Duration::from_millis(16),
186 mouse: false,
187 kitty_keyboard: false,
188 theme: Theme::dark(),
189 color_depth: None,
190 max_fps: Some(60),
191 }
192 }
193}
194
195pub fn run(f: impl FnMut(&mut Context)) -> io::Result<()> {
210 run_with(RunConfig::default(), f)
211}
212
213pub fn run_with(config: RunConfig, mut f: impl FnMut(&mut Context)) -> io::Result<()> {
233 if !io::stdout().is_terminal() {
234 return Ok(());
235 }
236
237 install_panic_hook();
238 let color_depth = config.color_depth.unwrap_or_else(ColorDepth::detect);
239 let mut term = Terminal::new(config.mouse, config.kitty_keyboard, color_depth)?;
240 let mut events: Vec<Event> = Vec::new();
241 let mut debug_mode: bool = false;
242 let mut tick: u64 = 0;
243 let mut focus_index: usize = 0;
244 let mut prev_focus_count: usize = 0;
245 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
246 let mut prev_scroll_rects: Vec<rect::Rect> = Vec::new();
247 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
248 let mut prev_group_rects: Vec<(String, rect::Rect)> = Vec::new();
249 let mut prev_content_map: Vec<(rect::Rect, rect::Rect)> = Vec::new();
250 let mut prev_focus_rects: Vec<(usize, rect::Rect)> = Vec::new();
251 let mut prev_focus_groups: Vec<Option<String>> = Vec::new();
252 let mut hook_states: Vec<Box<dyn std::any::Any>> = Vec::new();
253 let mut last_mouse_pos: Option<(u32, u32)> = None;
254 let mut prev_modal_active = false;
255 let mut selection = terminal::SelectionState::default();
256 let mut fps_ema: f32 = 0.0;
257
258 loop {
259 let frame_start = Instant::now();
260 let (w, h) = term.size();
261 if w == 0 || h == 0 {
262 sleep_for_fps_cap(config.max_fps, frame_start);
263 continue;
264 }
265 let mut ctx = Context::new(
266 std::mem::take(&mut events),
267 w,
268 h,
269 tick,
270 focus_index,
271 prev_focus_count,
272 std::mem::take(&mut prev_scroll_infos),
273 std::mem::take(&mut prev_scroll_rects),
274 std::mem::take(&mut prev_hit_map),
275 std::mem::take(&mut prev_group_rects),
276 std::mem::take(&mut prev_focus_rects),
277 std::mem::take(&mut prev_focus_groups),
278 std::mem::take(&mut hook_states),
279 debug_mode,
280 config.theme,
281 last_mouse_pos,
282 prev_modal_active,
283 );
284 ctx.process_focus_keys();
285
286 f(&mut ctx);
287
288 if ctx.should_quit {
289 break;
290 }
291 prev_modal_active = ctx.modal_active;
292 let clipboard_text = ctx.clipboard_text.take();
293
294 let mut should_copy_selection = false;
295 for ev in ctx.events.iter() {
296 if let Event::Mouse(mouse) = ev {
297 match mouse.kind {
298 event::MouseKind::Down(event::MouseButton::Left) => {
299 selection.mouse_down(mouse.x, mouse.y, &prev_content_map);
300 }
301 event::MouseKind::Drag(event::MouseButton::Left) => {
302 selection.mouse_drag(mouse.x, mouse.y, &prev_content_map);
303 }
304 event::MouseKind::Up(event::MouseButton::Left) => {
305 should_copy_selection = selection.active;
306 }
307 _ => {}
308 }
309 }
310 }
311
312 focus_index = ctx.focus_index;
313 prev_focus_count = ctx.focus_count;
314
315 let mut tree = layout::build_tree(&ctx.commands);
316 let area = crate::rect::Rect::new(0, 0, w, h);
317 layout::compute(&mut tree, area);
318 prev_scroll_infos = layout::collect_scroll_infos(&tree);
319 prev_scroll_rects = layout::collect_scroll_rects(&tree);
320 prev_hit_map = layout::collect_hit_areas(&tree);
321 prev_group_rects = layout::collect_group_rects(&tree);
322 prev_content_map = layout::collect_content_areas(&tree);
323 prev_focus_rects = layout::collect_focus_rects(&tree);
324 prev_focus_groups = layout::collect_focus_groups(&tree);
325 layout::render(&tree, term.buffer_mut());
326 hook_states = ctx.hook_states;
327 let frame_time = frame_start.elapsed();
328 let frame_time_us = frame_time.as_micros().min(u128::from(u64::MAX)) as u64;
329 let frame_secs = frame_time.as_secs_f32();
330 let inst_fps = if frame_secs > 0.0 {
331 1.0 / frame_secs
332 } else {
333 0.0
334 };
335 fps_ema = if fps_ema == 0.0 {
336 inst_fps
337 } else {
338 (fps_ema * 0.9) + (inst_fps * 0.1)
339 };
340 if debug_mode {
341 layout::render_debug_overlay(&tree, term.buffer_mut(), frame_time_us, fps_ema);
342 }
343
344 if selection.active {
345 terminal::apply_selection_overlay(term.buffer_mut(), &selection, &prev_content_map);
346 }
347 if should_copy_selection {
348 let text =
349 terminal::extract_selection_text(term.buffer_mut(), &selection, &prev_content_map);
350 if !text.is_empty() {
351 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
352 }
353 selection.clear();
354 }
355
356 term.flush()?;
357 if let Some(text) = clipboard_text {
358 let _ = terminal::copy_to_clipboard(&mut io::stdout(), &text);
359 }
360 tick = tick.wrapping_add(1);
361
362 events.clear();
363 if crossterm::event::poll(config.tick_rate)? {
364 let raw = crossterm::event::read()?;
365 if let Some(ev) = event::from_crossterm(raw) {
366 if is_ctrl_c(&ev) {
367 break;
368 }
369 if let Event::Resize(_, _) = &ev {
370 term.handle_resize()?;
371 }
372 events.push(ev);
373 }
374
375 while crossterm::event::poll(Duration::ZERO)? {
376 let raw = crossterm::event::read()?;
377 if let Some(ev) = event::from_crossterm(raw) {
378 if is_ctrl_c(&ev) {
379 return Ok(());
380 }
381 if let Event::Resize(_, _) = &ev {
382 term.handle_resize()?;
383 }
384 events.push(ev);
385 }
386 }
387
388 for ev in &events {
389 if matches!(
390 ev,
391 Event::Key(event::KeyEvent {
392 code: KeyCode::F(12),
393 kind: event::KeyEventKind::Press,
394 ..
395 })
396 ) {
397 debug_mode = !debug_mode;
398 }
399 }
400 }
401
402 for ev in &events {
403 match ev {
404 Event::Mouse(mouse) => {
405 last_mouse_pos = Some((mouse.x, mouse.y));
406 }
407 Event::FocusLost => {
408 last_mouse_pos = None;
409 }
410 _ => {}
411 }
412 }
413
414 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
415 prev_hit_map.clear();
416 prev_group_rects.clear();
417 prev_content_map.clear();
418 prev_focus_rects.clear();
419 prev_focus_groups.clear();
420 prev_scroll_infos.clear();
421 prev_scroll_rects.clear();
422 last_mouse_pos = None;
423 }
424
425 sleep_for_fps_cap(config.max_fps, frame_start);
426 }
427
428 Ok(())
429}
430
431#[cfg(feature = "async")]
452pub fn run_async<M: Send + 'static>(
453 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
454) -> io::Result<tokio::sync::mpsc::Sender<M>> {
455 run_async_with(RunConfig::default(), f)
456}
457
458#[cfg(feature = "async")]
465pub fn run_async_with<M: Send + 'static>(
466 config: RunConfig,
467 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
468) -> io::Result<tokio::sync::mpsc::Sender<M>> {
469 let (tx, rx) = tokio::sync::mpsc::channel(100);
470 let handle =
471 tokio::runtime::Handle::try_current().map_err(|err| io::Error::other(err.to_string()))?;
472
473 handle.spawn_blocking(move || {
474 let _ = run_async_loop(config, f, rx);
475 });
476
477 Ok(tx)
478}
479
480#[cfg(feature = "async")]
481fn run_async_loop<M: Send + 'static>(
482 config: RunConfig,
483 mut f: impl FnMut(&mut Context, &mut Vec<M>) + Send,
484 mut rx: tokio::sync::mpsc::Receiver<M>,
485) -> io::Result<()> {
486 if !io::stdout().is_terminal() {
487 return Ok(());
488 }
489
490 install_panic_hook();
491 let color_depth = config.color_depth.unwrap_or_else(ColorDepth::detect);
492 let mut term = Terminal::new(config.mouse, config.kitty_keyboard, color_depth)?;
493 let mut events: Vec<Event> = Vec::new();
494 let mut tick: u64 = 0;
495 let mut focus_index: usize = 0;
496 let mut prev_focus_count: usize = 0;
497 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
498 let mut prev_scroll_rects: Vec<rect::Rect> = Vec::new();
499 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
500 let mut prev_group_rects: Vec<(String, rect::Rect)> = Vec::new();
501 let mut prev_content_map: Vec<(rect::Rect, rect::Rect)> = Vec::new();
502 let mut prev_focus_rects: Vec<(usize, rect::Rect)> = Vec::new();
503 let mut prev_focus_groups: Vec<Option<String>> = Vec::new();
504 let mut hook_states: Vec<Box<dyn std::any::Any>> = Vec::new();
505 let mut last_mouse_pos: Option<(u32, u32)> = None;
506 let mut prev_modal_active = false;
507 let mut selection = terminal::SelectionState::default();
508
509 loop {
510 let frame_start = Instant::now();
511 let mut messages: Vec<M> = Vec::new();
512 while let Ok(message) = rx.try_recv() {
513 messages.push(message);
514 }
515
516 let (w, h) = term.size();
517 if w == 0 || h == 0 {
518 sleep_for_fps_cap(config.max_fps, frame_start);
519 continue;
520 }
521 let mut ctx = Context::new(
522 std::mem::take(&mut events),
523 w,
524 h,
525 tick,
526 focus_index,
527 prev_focus_count,
528 std::mem::take(&mut prev_scroll_infos),
529 std::mem::take(&mut prev_scroll_rects),
530 std::mem::take(&mut prev_hit_map),
531 std::mem::take(&mut prev_group_rects),
532 std::mem::take(&mut prev_focus_rects),
533 std::mem::take(&mut prev_focus_groups),
534 std::mem::take(&mut hook_states),
535 false,
536 config.theme,
537 last_mouse_pos,
538 prev_modal_active,
539 );
540 ctx.process_focus_keys();
541
542 f(&mut ctx, &mut messages);
543
544 if ctx.should_quit {
545 break;
546 }
547 prev_modal_active = ctx.modal_active;
548 let clipboard_text = ctx.clipboard_text.take();
549
550 let mut should_copy_selection = false;
551 for ev in ctx.events.iter() {
552 if let Event::Mouse(mouse) = ev {
553 match mouse.kind {
554 event::MouseKind::Down(event::MouseButton::Left) => {
555 selection.mouse_down(mouse.x, mouse.y, &prev_content_map);
556 }
557 event::MouseKind::Drag(event::MouseButton::Left) => {
558 selection.mouse_drag(mouse.x, mouse.y, &prev_content_map);
559 }
560 event::MouseKind::Up(event::MouseButton::Left) => {
561 should_copy_selection = selection.active;
562 }
563 _ => {}
564 }
565 }
566 }
567
568 focus_index = ctx.focus_index;
569 prev_focus_count = ctx.focus_count;
570
571 let mut tree = layout::build_tree(&ctx.commands);
572 let area = crate::rect::Rect::new(0, 0, w, h);
573 layout::compute(&mut tree, area);
574 prev_scroll_infos = layout::collect_scroll_infos(&tree);
575 prev_scroll_rects = layout::collect_scroll_rects(&tree);
576 prev_hit_map = layout::collect_hit_areas(&tree);
577 prev_group_rects = layout::collect_group_rects(&tree);
578 prev_content_map = layout::collect_content_areas(&tree);
579 prev_focus_rects = layout::collect_focus_rects(&tree);
580 prev_focus_groups = layout::collect_focus_groups(&tree);
581 layout::render(&tree, term.buffer_mut());
582 hook_states = ctx.hook_states;
583
584 if selection.active {
585 terminal::apply_selection_overlay(term.buffer_mut(), &selection, &prev_content_map);
586 }
587 if should_copy_selection {
588 let text =
589 terminal::extract_selection_text(term.buffer_mut(), &selection, &prev_content_map);
590 if !text.is_empty() {
591 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
592 }
593 selection.clear();
594 }
595
596 term.flush()?;
597 if let Some(text) = clipboard_text {
598 let _ = terminal::copy_to_clipboard(&mut io::stdout(), &text);
599 }
600 tick = tick.wrapping_add(1);
601
602 events.clear();
603 if crossterm::event::poll(config.tick_rate)? {
604 let raw = crossterm::event::read()?;
605 if let Some(ev) = event::from_crossterm(raw) {
606 if is_ctrl_c(&ev) {
607 break;
608 }
609 if let Event::Resize(_, _) = &ev {
610 term.handle_resize()?;
611 prev_hit_map.clear();
612 prev_group_rects.clear();
613 prev_content_map.clear();
614 prev_focus_rects.clear();
615 prev_focus_groups.clear();
616 prev_scroll_infos.clear();
617 prev_scroll_rects.clear();
618 last_mouse_pos = None;
619 }
620 events.push(ev);
621 }
622
623 while crossterm::event::poll(Duration::ZERO)? {
624 let raw = crossterm::event::read()?;
625 if let Some(ev) = event::from_crossterm(raw) {
626 if is_ctrl_c(&ev) {
627 return Ok(());
628 }
629 if let Event::Resize(_, _) = &ev {
630 term.handle_resize()?;
631 prev_hit_map.clear();
632 prev_group_rects.clear();
633 prev_content_map.clear();
634 prev_focus_rects.clear();
635 prev_focus_groups.clear();
636 prev_scroll_infos.clear();
637 prev_scroll_rects.clear();
638 last_mouse_pos = None;
639 }
640 events.push(ev);
641 }
642 }
643 }
644
645 for ev in &events {
646 match ev {
647 Event::Mouse(mouse) => {
648 last_mouse_pos = Some((mouse.x, mouse.y));
649 }
650 Event::FocusLost => {
651 last_mouse_pos = None;
652 }
653 _ => {}
654 }
655 }
656
657 sleep_for_fps_cap(config.max_fps, frame_start);
658 }
659
660 Ok(())
661}
662
663pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
679 run_inline_with(height, RunConfig::default(), f)
680}
681
682pub fn run_inline_with(
687 height: u32,
688 config: RunConfig,
689 mut f: impl FnMut(&mut Context),
690) -> io::Result<()> {
691 if !io::stdout().is_terminal() {
692 return Ok(());
693 }
694
695 install_panic_hook();
696 let color_depth = config.color_depth.unwrap_or_else(ColorDepth::detect);
697 let mut term = InlineTerminal::new(height, config.mouse, color_depth)?;
698 let mut events: Vec<Event> = Vec::new();
699 let mut debug_mode: bool = false;
700 let mut tick: u64 = 0;
701 let mut focus_index: usize = 0;
702 let mut prev_focus_count: usize = 0;
703 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
704 let mut prev_scroll_rects: Vec<rect::Rect> = Vec::new();
705 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
706 let mut prev_group_rects: Vec<(String, rect::Rect)> = Vec::new();
707 let mut prev_content_map: Vec<(rect::Rect, rect::Rect)> = Vec::new();
708 let mut prev_focus_rects: Vec<(usize, rect::Rect)> = Vec::new();
709 let mut prev_focus_groups: Vec<Option<String>> = Vec::new();
710 let mut hook_states: Vec<Box<dyn std::any::Any>> = Vec::new();
711 let mut last_mouse_pos: Option<(u32, u32)> = None;
712 let mut prev_modal_active = false;
713 let mut selection = terminal::SelectionState::default();
714 let mut fps_ema: f32 = 0.0;
715
716 loop {
717 let frame_start = Instant::now();
718 let (w, h) = term.size();
719 if w == 0 || h == 0 {
720 sleep_for_fps_cap(config.max_fps, frame_start);
721 continue;
722 }
723 let mut ctx = Context::new(
724 std::mem::take(&mut events),
725 w,
726 h,
727 tick,
728 focus_index,
729 prev_focus_count,
730 std::mem::take(&mut prev_scroll_infos),
731 std::mem::take(&mut prev_scroll_rects),
732 std::mem::take(&mut prev_hit_map),
733 std::mem::take(&mut prev_group_rects),
734 std::mem::take(&mut prev_focus_rects),
735 std::mem::take(&mut prev_focus_groups),
736 std::mem::take(&mut hook_states),
737 debug_mode,
738 config.theme,
739 last_mouse_pos,
740 prev_modal_active,
741 );
742 ctx.process_focus_keys();
743
744 f(&mut ctx);
745
746 if ctx.should_quit {
747 break;
748 }
749 prev_modal_active = ctx.modal_active;
750 let clipboard_text = ctx.clipboard_text.take();
751
752 let mut should_copy_selection = false;
753 for ev in ctx.events.iter() {
754 if let Event::Mouse(mouse) = ev {
755 match mouse.kind {
756 event::MouseKind::Down(event::MouseButton::Left) => {
757 selection.mouse_down(mouse.x, mouse.y, &prev_content_map);
758 }
759 event::MouseKind::Drag(event::MouseButton::Left) => {
760 selection.mouse_drag(mouse.x, mouse.y, &prev_content_map);
761 }
762 event::MouseKind::Up(event::MouseButton::Left) => {
763 should_copy_selection = selection.active;
764 }
765 _ => {}
766 }
767 }
768 }
769
770 focus_index = ctx.focus_index;
771 prev_focus_count = ctx.focus_count;
772
773 let mut tree = layout::build_tree(&ctx.commands);
774 let area = crate::rect::Rect::new(0, 0, w, h);
775 layout::compute(&mut tree, area);
776 prev_scroll_infos = layout::collect_scroll_infos(&tree);
777 prev_scroll_rects = layout::collect_scroll_rects(&tree);
778 prev_hit_map = layout::collect_hit_areas(&tree);
779 prev_group_rects = layout::collect_group_rects(&tree);
780 prev_content_map = layout::collect_content_areas(&tree);
781 prev_focus_rects = layout::collect_focus_rects(&tree);
782 prev_focus_groups = layout::collect_focus_groups(&tree);
783 layout::render(&tree, term.buffer_mut());
784 hook_states = ctx.hook_states;
785 let frame_time = frame_start.elapsed();
786 let frame_time_us = frame_time.as_micros().min(u128::from(u64::MAX)) as u64;
787 let frame_secs = frame_time.as_secs_f32();
788 let inst_fps = if frame_secs > 0.0 {
789 1.0 / frame_secs
790 } else {
791 0.0
792 };
793 fps_ema = if fps_ema == 0.0 {
794 inst_fps
795 } else {
796 (fps_ema * 0.9) + (inst_fps * 0.1)
797 };
798 if debug_mode {
799 layout::render_debug_overlay(&tree, term.buffer_mut(), frame_time_us, fps_ema);
800 }
801
802 if selection.active {
803 terminal::apply_selection_overlay(term.buffer_mut(), &selection, &prev_content_map);
804 }
805 if should_copy_selection {
806 let text =
807 terminal::extract_selection_text(term.buffer_mut(), &selection, &prev_content_map);
808 if !text.is_empty() {
809 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
810 }
811 selection.clear();
812 }
813
814 term.flush()?;
815 if let Some(text) = clipboard_text {
816 let _ = terminal::copy_to_clipboard(&mut io::stdout(), &text);
817 }
818 tick = tick.wrapping_add(1);
819
820 events.clear();
821 if crossterm::event::poll(config.tick_rate)? {
822 let raw = crossterm::event::read()?;
823 if let Some(ev) = event::from_crossterm(raw) {
824 if is_ctrl_c(&ev) {
825 break;
826 }
827 if let Event::Resize(_, _) = &ev {
828 term.handle_resize()?;
829 }
830 events.push(ev);
831 }
832
833 while crossterm::event::poll(Duration::ZERO)? {
834 let raw = crossterm::event::read()?;
835 if let Some(ev) = event::from_crossterm(raw) {
836 if is_ctrl_c(&ev) {
837 return Ok(());
838 }
839 if let Event::Resize(_, _) = &ev {
840 term.handle_resize()?;
841 }
842 events.push(ev);
843 }
844 }
845
846 for ev in &events {
847 if matches!(
848 ev,
849 Event::Key(event::KeyEvent {
850 code: KeyCode::F(12),
851 kind: event::KeyEventKind::Press,
852 ..
853 })
854 ) {
855 debug_mode = !debug_mode;
856 }
857 }
858 }
859
860 for ev in &events {
861 match ev {
862 Event::Mouse(mouse) => {
863 last_mouse_pos = Some((mouse.x, mouse.y));
864 }
865 Event::FocusLost => {
866 last_mouse_pos = None;
867 }
868 _ => {}
869 }
870 }
871
872 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
873 prev_hit_map.clear();
874 prev_group_rects.clear();
875 prev_content_map.clear();
876 prev_focus_rects.clear();
877 prev_focus_groups.clear();
878 prev_scroll_infos.clear();
879 prev_scroll_rects.clear();
880 last_mouse_pos = None;
881 }
882
883 sleep_for_fps_cap(config.max_fps, frame_start);
884 }
885
886 Ok(())
887}
888
889fn is_ctrl_c(ev: &Event) -> bool {
890 matches!(
891 ev,
892 Event::Key(event::KeyEvent {
893 code: KeyCode::Char('c'),
894 modifiers,
895 kind: event::KeyEventKind::Press,
896 }) if modifiers.contains(KeyModifiers::CONTROL)
897 )
898}
899
900fn sleep_for_fps_cap(max_fps: Option<u32>, frame_start: Instant) {
901 if let Some(fps) = max_fps.filter(|fps| *fps > 0) {
902 let target = Duration::from_secs_f64(1.0 / fps as f64);
903 let elapsed = frame_start.elapsed();
904 if elapsed < target {
905 std::thread::sleep(target - elapsed);
906 }
907 }
908}