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, 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,
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_content_map: Vec<(rect::Rect, rect::Rect)> = Vec::new();
249 let mut prev_focus_rects: Vec<(usize, rect::Rect)> = Vec::new();
250 let mut last_mouse_pos: Option<(u32, u32)> = None;
251 let mut prev_modal_active = false;
252 let mut selection = terminal::SelectionState::default();
253 let mut fps_ema: f32 = 0.0;
254
255 loop {
256 let frame_start = Instant::now();
257 let (w, h) = term.size();
258 if w == 0 || h == 0 {
259 sleep_for_fps_cap(config.max_fps, frame_start);
260 continue;
261 }
262 let mut ctx = Context::new(
263 std::mem::take(&mut events),
264 w,
265 h,
266 tick,
267 focus_index,
268 prev_focus_count,
269 std::mem::take(&mut prev_scroll_infos),
270 std::mem::take(&mut prev_scroll_rects),
271 std::mem::take(&mut prev_hit_map),
272 std::mem::take(&mut prev_focus_rects),
273 debug_mode,
274 config.theme,
275 last_mouse_pos,
276 prev_modal_active,
277 );
278 ctx.process_focus_keys();
279
280 f(&mut ctx);
281
282 if ctx.should_quit {
283 break;
284 }
285 prev_modal_active = ctx.modal_active;
286 let clipboard_text = ctx.clipboard_text.take();
287
288 let mut should_copy_selection = false;
289 for ev in ctx.events.iter() {
290 if let Event::Mouse(mouse) = ev {
291 match mouse.kind {
292 event::MouseKind::Down(event::MouseButton::Left) => {
293 selection.mouse_down(mouse.x, mouse.y, &prev_content_map);
294 }
295 event::MouseKind::Drag(event::MouseButton::Left) => {
296 selection.mouse_drag(mouse.x, mouse.y, &prev_content_map);
297 }
298 event::MouseKind::Up(event::MouseButton::Left) => {
299 should_copy_selection = selection.active;
300 }
301 _ => {}
302 }
303 }
304 }
305
306 focus_index = ctx.focus_index;
307 prev_focus_count = ctx.focus_count;
308
309 let mut tree = layout::build_tree(&ctx.commands);
310 let area = crate::rect::Rect::new(0, 0, w, h);
311 layout::compute(&mut tree, area);
312 prev_scroll_infos = layout::collect_scroll_infos(&tree);
313 prev_scroll_rects = layout::collect_scroll_rects(&tree);
314 prev_hit_map = layout::collect_hit_areas(&tree);
315 prev_content_map = layout::collect_content_areas(&tree);
316 prev_focus_rects = layout::collect_focus_rects(&tree);
317 layout::render(&tree, term.buffer_mut());
318 let frame_time = frame_start.elapsed();
319 let frame_time_us = frame_time.as_micros().min(u128::from(u64::MAX)) as u64;
320 let frame_secs = frame_time.as_secs_f32();
321 let inst_fps = if frame_secs > 0.0 {
322 1.0 / frame_secs
323 } else {
324 0.0
325 };
326 fps_ema = if fps_ema == 0.0 {
327 inst_fps
328 } else {
329 (fps_ema * 0.9) + (inst_fps * 0.1)
330 };
331 if debug_mode {
332 layout::render_debug_overlay(&tree, term.buffer_mut(), frame_time_us, fps_ema);
333 }
334
335 if selection.active {
336 terminal::apply_selection_overlay(term.buffer_mut(), &selection, &prev_content_map);
337 }
338 if should_copy_selection {
339 let text =
340 terminal::extract_selection_text(term.buffer_mut(), &selection, &prev_content_map);
341 if !text.is_empty() {
342 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
343 }
344 selection.clear();
345 }
346
347 term.flush()?;
348 if let Some(text) = clipboard_text {
349 let _ = terminal::copy_to_clipboard(&mut io::stdout(), &text);
350 }
351 tick = tick.wrapping_add(1);
352
353 events.clear();
354 if crossterm::event::poll(config.tick_rate)? {
355 let raw = crossterm::event::read()?;
356 if let Some(ev) = event::from_crossterm(raw) {
357 if is_ctrl_c(&ev) {
358 break;
359 }
360 if let Event::Resize(_, _) = &ev {
361 term.handle_resize()?;
362 }
363 events.push(ev);
364 }
365
366 while crossterm::event::poll(Duration::ZERO)? {
367 let raw = crossterm::event::read()?;
368 if let Some(ev) = event::from_crossterm(raw) {
369 if is_ctrl_c(&ev) {
370 return Ok(());
371 }
372 if let Event::Resize(_, _) = &ev {
373 term.handle_resize()?;
374 }
375 events.push(ev);
376 }
377 }
378
379 for ev in &events {
380 if matches!(
381 ev,
382 Event::Key(event::KeyEvent {
383 code: KeyCode::F(12),
384 kind: event::KeyEventKind::Press,
385 ..
386 })
387 ) {
388 debug_mode = !debug_mode;
389 }
390 }
391 }
392
393 for ev in &events {
394 match ev {
395 Event::Mouse(mouse) => {
396 last_mouse_pos = Some((mouse.x, mouse.y));
397 }
398 Event::FocusLost => {
399 last_mouse_pos = None;
400 }
401 _ => {}
402 }
403 }
404
405 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
406 prev_hit_map.clear();
407 prev_content_map.clear();
408 prev_focus_rects.clear();
409 prev_scroll_infos.clear();
410 prev_scroll_rects.clear();
411 last_mouse_pos = None;
412 }
413
414 sleep_for_fps_cap(config.max_fps, frame_start);
415 }
416
417 Ok(())
418}
419
420#[cfg(feature = "async")]
441pub fn run_async<M: Send + 'static>(
442 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
443) -> io::Result<tokio::sync::mpsc::Sender<M>> {
444 run_async_with(RunConfig::default(), f)
445}
446
447#[cfg(feature = "async")]
454pub fn run_async_with<M: Send + 'static>(
455 config: RunConfig,
456 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
457) -> io::Result<tokio::sync::mpsc::Sender<M>> {
458 let (tx, rx) = tokio::sync::mpsc::channel(100);
459 let handle =
460 tokio::runtime::Handle::try_current().map_err(|err| io::Error::other(err.to_string()))?;
461
462 handle.spawn_blocking(move || {
463 let _ = run_async_loop(config, f, rx);
464 });
465
466 Ok(tx)
467}
468
469#[cfg(feature = "async")]
470fn run_async_loop<M: Send + 'static>(
471 config: RunConfig,
472 mut f: impl FnMut(&mut Context, &mut Vec<M>) + Send,
473 mut rx: tokio::sync::mpsc::Receiver<M>,
474) -> io::Result<()> {
475 if !io::stdout().is_terminal() {
476 return Ok(());
477 }
478
479 install_panic_hook();
480 let color_depth = config.color_depth.unwrap_or_else(ColorDepth::detect);
481 let mut term = Terminal::new(config.mouse, config.kitty_keyboard, color_depth)?;
482 let mut events: Vec<Event> = Vec::new();
483 let mut tick: u64 = 0;
484 let mut focus_index: usize = 0;
485 let mut prev_focus_count: usize = 0;
486 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
487 let mut prev_scroll_rects: Vec<rect::Rect> = Vec::new();
488 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
489 let mut prev_content_map: Vec<(rect::Rect, rect::Rect)> = Vec::new();
490 let mut prev_focus_rects: Vec<(usize, rect::Rect)> = Vec::new();
491 let mut last_mouse_pos: Option<(u32, u32)> = None;
492 let mut prev_modal_active = false;
493 let mut selection = terminal::SelectionState::default();
494
495 loop {
496 let frame_start = Instant::now();
497 let mut messages: Vec<M> = Vec::new();
498 while let Ok(message) = rx.try_recv() {
499 messages.push(message);
500 }
501
502 let (w, h) = term.size();
503 if w == 0 || h == 0 {
504 sleep_for_fps_cap(config.max_fps, frame_start);
505 continue;
506 }
507 let mut ctx = Context::new(
508 std::mem::take(&mut events),
509 w,
510 h,
511 tick,
512 focus_index,
513 prev_focus_count,
514 std::mem::take(&mut prev_scroll_infos),
515 std::mem::take(&mut prev_scroll_rects),
516 std::mem::take(&mut prev_hit_map),
517 std::mem::take(&mut prev_focus_rects),
518 false,
519 config.theme,
520 last_mouse_pos,
521 prev_modal_active,
522 );
523 ctx.process_focus_keys();
524
525 f(&mut ctx, &mut messages);
526
527 if ctx.should_quit {
528 break;
529 }
530 prev_modal_active = ctx.modal_active;
531 let clipboard_text = ctx.clipboard_text.take();
532
533 let mut should_copy_selection = false;
534 for ev in ctx.events.iter() {
535 if let Event::Mouse(mouse) = ev {
536 match mouse.kind {
537 event::MouseKind::Down(event::MouseButton::Left) => {
538 selection.mouse_down(mouse.x, mouse.y, &prev_content_map);
539 }
540 event::MouseKind::Drag(event::MouseButton::Left) => {
541 selection.mouse_drag(mouse.x, mouse.y, &prev_content_map);
542 }
543 event::MouseKind::Up(event::MouseButton::Left) => {
544 should_copy_selection = selection.active;
545 }
546 _ => {}
547 }
548 }
549 }
550
551 focus_index = ctx.focus_index;
552 prev_focus_count = ctx.focus_count;
553
554 let mut tree = layout::build_tree(&ctx.commands);
555 let area = crate::rect::Rect::new(0, 0, w, h);
556 layout::compute(&mut tree, area);
557 prev_scroll_infos = layout::collect_scroll_infos(&tree);
558 prev_scroll_rects = layout::collect_scroll_rects(&tree);
559 prev_hit_map = layout::collect_hit_areas(&tree);
560 prev_content_map = layout::collect_content_areas(&tree);
561 prev_focus_rects = layout::collect_focus_rects(&tree);
562 layout::render(&tree, term.buffer_mut());
563
564 if selection.active {
565 terminal::apply_selection_overlay(term.buffer_mut(), &selection, &prev_content_map);
566 }
567 if should_copy_selection {
568 let text =
569 terminal::extract_selection_text(term.buffer_mut(), &selection, &prev_content_map);
570 if !text.is_empty() {
571 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
572 }
573 selection.clear();
574 }
575
576 term.flush()?;
577 if let Some(text) = clipboard_text {
578 let _ = terminal::copy_to_clipboard(&mut io::stdout(), &text);
579 }
580 tick = tick.wrapping_add(1);
581
582 events.clear();
583 if crossterm::event::poll(config.tick_rate)? {
584 let raw = crossterm::event::read()?;
585 if let Some(ev) = event::from_crossterm(raw) {
586 if is_ctrl_c(&ev) {
587 break;
588 }
589 if let Event::Resize(_, _) = &ev {
590 term.handle_resize()?;
591 prev_hit_map.clear();
592 prev_content_map.clear();
593 prev_focus_rects.clear();
594 prev_scroll_infos.clear();
595 prev_scroll_rects.clear();
596 last_mouse_pos = None;
597 }
598 events.push(ev);
599 }
600
601 while crossterm::event::poll(Duration::ZERO)? {
602 let raw = crossterm::event::read()?;
603 if let Some(ev) = event::from_crossterm(raw) {
604 if is_ctrl_c(&ev) {
605 return Ok(());
606 }
607 if let Event::Resize(_, _) = &ev {
608 term.handle_resize()?;
609 prev_hit_map.clear();
610 prev_content_map.clear();
611 prev_focus_rects.clear();
612 prev_scroll_infos.clear();
613 prev_scroll_rects.clear();
614 last_mouse_pos = None;
615 }
616 events.push(ev);
617 }
618 }
619 }
620
621 for ev in &events {
622 match ev {
623 Event::Mouse(mouse) => {
624 last_mouse_pos = Some((mouse.x, mouse.y));
625 }
626 Event::FocusLost => {
627 last_mouse_pos = None;
628 }
629 _ => {}
630 }
631 }
632
633 sleep_for_fps_cap(config.max_fps, frame_start);
634 }
635
636 Ok(())
637}
638
639pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
655 run_inline_with(height, RunConfig::default(), f)
656}
657
658pub fn run_inline_with(
663 height: u32,
664 config: RunConfig,
665 mut f: impl FnMut(&mut Context),
666) -> io::Result<()> {
667 if !io::stdout().is_terminal() {
668 return Ok(());
669 }
670
671 install_panic_hook();
672 let color_depth = config.color_depth.unwrap_or_else(ColorDepth::detect);
673 let mut term = InlineTerminal::new(height, config.mouse, color_depth)?;
674 let mut events: Vec<Event> = Vec::new();
675 let mut debug_mode: bool = false;
676 let mut tick: u64 = 0;
677 let mut focus_index: usize = 0;
678 let mut prev_focus_count: usize = 0;
679 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
680 let mut prev_scroll_rects: Vec<rect::Rect> = Vec::new();
681 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
682 let mut prev_content_map: Vec<(rect::Rect, rect::Rect)> = Vec::new();
683 let mut prev_focus_rects: Vec<(usize, rect::Rect)> = Vec::new();
684 let mut last_mouse_pos: Option<(u32, u32)> = None;
685 let mut prev_modal_active = false;
686 let mut selection = terminal::SelectionState::default();
687 let mut fps_ema: f32 = 0.0;
688
689 loop {
690 let frame_start = Instant::now();
691 let (w, h) = term.size();
692 if w == 0 || h == 0 {
693 sleep_for_fps_cap(config.max_fps, frame_start);
694 continue;
695 }
696 let mut ctx = Context::new(
697 std::mem::take(&mut events),
698 w,
699 h,
700 tick,
701 focus_index,
702 prev_focus_count,
703 std::mem::take(&mut prev_scroll_infos),
704 std::mem::take(&mut prev_scroll_rects),
705 std::mem::take(&mut prev_hit_map),
706 std::mem::take(&mut prev_focus_rects),
707 debug_mode,
708 config.theme,
709 last_mouse_pos,
710 prev_modal_active,
711 );
712 ctx.process_focus_keys();
713
714 f(&mut ctx);
715
716 if ctx.should_quit {
717 break;
718 }
719 prev_modal_active = ctx.modal_active;
720 let clipboard_text = ctx.clipboard_text.take();
721
722 let mut should_copy_selection = false;
723 for ev in ctx.events.iter() {
724 if let Event::Mouse(mouse) = ev {
725 match mouse.kind {
726 event::MouseKind::Down(event::MouseButton::Left) => {
727 selection.mouse_down(mouse.x, mouse.y, &prev_content_map);
728 }
729 event::MouseKind::Drag(event::MouseButton::Left) => {
730 selection.mouse_drag(mouse.x, mouse.y, &prev_content_map);
731 }
732 event::MouseKind::Up(event::MouseButton::Left) => {
733 should_copy_selection = selection.active;
734 }
735 _ => {}
736 }
737 }
738 }
739
740 focus_index = ctx.focus_index;
741 prev_focus_count = ctx.focus_count;
742
743 let mut tree = layout::build_tree(&ctx.commands);
744 let area = crate::rect::Rect::new(0, 0, w, h);
745 layout::compute(&mut tree, area);
746 prev_scroll_infos = layout::collect_scroll_infos(&tree);
747 prev_scroll_rects = layout::collect_scroll_rects(&tree);
748 prev_hit_map = layout::collect_hit_areas(&tree);
749 prev_content_map = layout::collect_content_areas(&tree);
750 prev_focus_rects = layout::collect_focus_rects(&tree);
751 layout::render(&tree, term.buffer_mut());
752 let frame_time = frame_start.elapsed();
753 let frame_time_us = frame_time.as_micros().min(u128::from(u64::MAX)) as u64;
754 let frame_secs = frame_time.as_secs_f32();
755 let inst_fps = if frame_secs > 0.0 {
756 1.0 / frame_secs
757 } else {
758 0.0
759 };
760 fps_ema = if fps_ema == 0.0 {
761 inst_fps
762 } else {
763 (fps_ema * 0.9) + (inst_fps * 0.1)
764 };
765 if debug_mode {
766 layout::render_debug_overlay(&tree, term.buffer_mut(), frame_time_us, fps_ema);
767 }
768
769 if selection.active {
770 terminal::apply_selection_overlay(term.buffer_mut(), &selection, &prev_content_map);
771 }
772 if should_copy_selection {
773 let text =
774 terminal::extract_selection_text(term.buffer_mut(), &selection, &prev_content_map);
775 if !text.is_empty() {
776 terminal::copy_to_clipboard(&mut io::stdout(), &text)?;
777 }
778 selection.clear();
779 }
780
781 term.flush()?;
782 if let Some(text) = clipboard_text {
783 let _ = terminal::copy_to_clipboard(&mut io::stdout(), &text);
784 }
785 tick = tick.wrapping_add(1);
786
787 events.clear();
788 if crossterm::event::poll(config.tick_rate)? {
789 let raw = crossterm::event::read()?;
790 if let Some(ev) = event::from_crossterm(raw) {
791 if is_ctrl_c(&ev) {
792 break;
793 }
794 if let Event::Resize(_, _) = &ev {
795 term.handle_resize()?;
796 }
797 events.push(ev);
798 }
799
800 while crossterm::event::poll(Duration::ZERO)? {
801 let raw = crossterm::event::read()?;
802 if let Some(ev) = event::from_crossterm(raw) {
803 if is_ctrl_c(&ev) {
804 return Ok(());
805 }
806 if let Event::Resize(_, _) = &ev {
807 term.handle_resize()?;
808 }
809 events.push(ev);
810 }
811 }
812
813 for ev in &events {
814 if matches!(
815 ev,
816 Event::Key(event::KeyEvent {
817 code: KeyCode::F(12),
818 kind: event::KeyEventKind::Press,
819 ..
820 })
821 ) {
822 debug_mode = !debug_mode;
823 }
824 }
825 }
826
827 for ev in &events {
828 match ev {
829 Event::Mouse(mouse) => {
830 last_mouse_pos = Some((mouse.x, mouse.y));
831 }
832 Event::FocusLost => {
833 last_mouse_pos = None;
834 }
835 _ => {}
836 }
837 }
838
839 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
840 prev_hit_map.clear();
841 prev_content_map.clear();
842 prev_focus_rects.clear();
843 prev_scroll_infos.clear();
844 prev_scroll_rects.clear();
845 last_mouse_pos = None;
846 }
847
848 sleep_for_fps_cap(config.max_fps, frame_start);
849 }
850
851 Ok(())
852}
853
854fn is_ctrl_c(ev: &Event) -> bool {
855 matches!(
856 ev,
857 Event::Key(event::KeyEvent {
858 code: KeyCode::Char('c'),
859 modifiers,
860 kind: event::KeyEventKind::Press,
861 }) if modifiers.contains(KeyModifiers::CONTROL)
862 )
863}
864
865fn sleep_for_fps_cap(max_fps: Option<u32>, frame_start: Instant) {
866 if let Some(fps) = max_fps.filter(|fps| *fps > 0) {
867 let target = Duration::from_secs_f64(1.0 / fps as f64);
868 let elapsed = frame_start.elapsed();
869 if elapsed < target {
870 std::thread::sleep(target - elapsed);
871 }
872 }
873}