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