1pub mod anim;
38pub mod buffer;
39pub mod cell;
40pub mod context;
41pub mod event;
42pub mod layout;
43pub mod rect;
44pub mod style;
45mod terminal;
46pub mod test_utils;
47pub mod widgets;
48
49use std::io;
50use std::sync::Once;
51use std::time::Duration;
52
53use event::Event;
54use terminal::{InlineTerminal, Terminal};
55
56pub use crate::test_utils::TestBackend;
57pub use anim::{Spring, Tween};
58pub use context::{Context, Response, Widget};
59pub use event::{KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseKind};
60pub use style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
61pub use widgets::{
62 ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
63 ToastLevel, ToastMessage, ToastState,
64};
65
66static PANIC_HOOK_ONCE: Once = Once::new();
67
68fn install_panic_hook() {
69 PANIC_HOOK_ONCE.call_once(|| {
70 let original = std::panic::take_hook();
71 std::panic::set_hook(Box::new(move |panic_info| {
72 let _ = crossterm::terminal::disable_raw_mode();
73 let mut stdout = io::stdout();
74 let _ = crossterm::execute!(
75 stdout,
76 crossterm::terminal::LeaveAlternateScreen,
77 crossterm::cursor::Show,
78 crossterm::event::DisableMouseCapture,
79 crossterm::style::ResetColor,
80 crossterm::style::SetAttribute(crossterm::style::Attribute::Reset)
81 );
82 original(panic_info);
83 }));
84 });
85}
86
87pub struct RunConfig {
105 pub tick_rate: Duration,
110 pub mouse: bool,
115 pub theme: Theme,
119}
120
121impl Default for RunConfig {
122 fn default() -> Self {
123 Self {
124 tick_rate: Duration::from_millis(100),
125 mouse: false,
126 theme: Theme::dark(),
127 }
128 }
129}
130
131pub fn run(f: impl FnMut(&mut Context)) -> io::Result<()> {
146 run_with(RunConfig::default(), f)
147}
148
149pub fn run_with(config: RunConfig, mut f: impl FnMut(&mut Context)) -> io::Result<()> {
169 install_panic_hook();
170 let mut term = Terminal::new(config.mouse)?;
171 let mut events: Vec<Event> = Vec::new();
172 let mut debug_mode: bool = false;
173 let mut tick: u64 = 0;
174 let mut focus_index: usize = 0;
175 let mut prev_focus_count: usize = 0;
176 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
177 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
178 let mut last_mouse_pos: Option<(u32, u32)> = None;
179
180 loop {
181 let (w, h) = term.size();
182 if w == 0 || h == 0 {
183 continue;
184 }
185 let mut ctx = Context::new(
186 std::mem::take(&mut events),
187 w,
188 h,
189 tick,
190 focus_index,
191 prev_focus_count,
192 std::mem::take(&mut prev_scroll_infos),
193 std::mem::take(&mut prev_hit_map),
194 debug_mode,
195 config.theme,
196 last_mouse_pos,
197 );
198 ctx.process_focus_keys();
199
200 f(&mut ctx);
201
202 if ctx.should_quit {
203 break;
204 }
205
206 focus_index = ctx.focus_index;
207 prev_focus_count = ctx.focus_count;
208
209 let mut tree = layout::build_tree(&ctx.commands);
210 let area = crate::rect::Rect::new(0, 0, w, h);
211 layout::compute(&mut tree, area);
212 prev_scroll_infos = layout::collect_scroll_infos(&tree);
213 prev_hit_map = layout::collect_hit_areas(&tree);
214 layout::render(&tree, term.buffer_mut());
215 if debug_mode {
216 layout::render_debug_overlay(&tree, term.buffer_mut());
217 }
218
219 term.flush()?;
220 tick = tick.wrapping_add(1);
221
222 events.clear();
223 if crossterm::event::poll(config.tick_rate)? {
224 let raw = crossterm::event::read()?;
225 if let Some(ev) = event::from_crossterm(raw) {
226 if is_ctrl_c(&ev) {
227 break;
228 }
229 if let Event::Resize(_, _) = &ev {
230 term.handle_resize()?;
231 }
232 events.push(ev);
233 }
234
235 while crossterm::event::poll(Duration::ZERO)? {
236 let raw = crossterm::event::read()?;
237 if let Some(ev) = event::from_crossterm(raw) {
238 if is_ctrl_c(&ev) {
239 return Ok(());
240 }
241 if let Event::Resize(_, _) = &ev {
242 term.handle_resize()?;
243 }
244 events.push(ev);
245 }
246 }
247
248 for ev in &events {
249 if matches!(
250 ev,
251 Event::Key(event::KeyEvent {
252 code: KeyCode::F(12),
253 ..
254 })
255 ) {
256 debug_mode = !debug_mode;
257 }
258 }
259 }
260
261 for ev in &events {
262 if let Event::Mouse(mouse) = ev {
263 last_mouse_pos = Some((mouse.x, mouse.y));
264 }
265 }
266
267 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
268 prev_hit_map.clear();
269 prev_scroll_infos.clear();
270 last_mouse_pos = None;
271 }
272 }
273
274 Ok(())
275}
276
277#[cfg(feature = "async")]
298pub fn run_async<M: Send + 'static>(
299 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
300) -> io::Result<tokio::sync::mpsc::Sender<M>> {
301 run_async_with(RunConfig::default(), f)
302}
303
304#[cfg(feature = "async")]
311pub fn run_async_with<M: Send + 'static>(
312 config: RunConfig,
313 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
314) -> io::Result<tokio::sync::mpsc::Sender<M>> {
315 let (tx, rx) = tokio::sync::mpsc::channel(100);
316 let handle =
317 tokio::runtime::Handle::try_current().map_err(|err| io::Error::other(err.to_string()))?;
318
319 handle.spawn_blocking(move || {
320 let _ = run_async_loop(config, f, rx);
321 });
322
323 Ok(tx)
324}
325
326#[cfg(feature = "async")]
327fn run_async_loop<M: Send + 'static>(
328 config: RunConfig,
329 mut f: impl FnMut(&mut Context, &mut Vec<M>) + Send,
330 mut rx: tokio::sync::mpsc::Receiver<M>,
331) -> io::Result<()> {
332 install_panic_hook();
333 let mut term = Terminal::new(config.mouse)?;
334 let mut events: Vec<Event> = Vec::new();
335 let mut tick: u64 = 0;
336 let mut focus_index: usize = 0;
337 let mut prev_focus_count: usize = 0;
338 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
339 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
340 let mut last_mouse_pos: Option<(u32, u32)> = None;
341
342 loop {
343 let mut messages: Vec<M> = Vec::new();
344 while let Ok(message) = rx.try_recv() {
345 messages.push(message);
346 }
347
348 let (w, h) = term.size();
349 if w == 0 || h == 0 {
350 continue;
351 }
352 let mut ctx = Context::new(
353 std::mem::take(&mut events),
354 w,
355 h,
356 tick,
357 focus_index,
358 prev_focus_count,
359 std::mem::take(&mut prev_scroll_infos),
360 std::mem::take(&mut prev_hit_map),
361 false,
362 config.theme,
363 last_mouse_pos,
364 );
365 ctx.process_focus_keys();
366
367 f(&mut ctx, &mut messages);
368
369 if ctx.should_quit {
370 break;
371 }
372
373 focus_index = ctx.focus_index;
374 prev_focus_count = ctx.focus_count;
375
376 let mut tree = layout::build_tree(&ctx.commands);
377 let area = crate::rect::Rect::new(0, 0, w, h);
378 layout::compute(&mut tree, area);
379 prev_scroll_infos = layout::collect_scroll_infos(&tree);
380 prev_hit_map = layout::collect_hit_areas(&tree);
381 layout::render(&tree, term.buffer_mut());
382
383 term.flush()?;
384 tick = tick.wrapping_add(1);
385
386 events.clear();
387 if crossterm::event::poll(config.tick_rate)? {
388 let raw = crossterm::event::read()?;
389 if let Some(ev) = event::from_crossterm(raw) {
390 if is_ctrl_c(&ev) {
391 break;
392 }
393 if let Event::Resize(_, _) = &ev {
394 term.handle_resize()?;
395 prev_hit_map.clear();
396 prev_scroll_infos.clear();
397 last_mouse_pos = None;
398 }
399 events.push(ev);
400 }
401
402 while crossterm::event::poll(Duration::ZERO)? {
403 let raw = crossterm::event::read()?;
404 if let Some(ev) = event::from_crossterm(raw) {
405 if is_ctrl_c(&ev) {
406 return Ok(());
407 }
408 if let Event::Resize(_, _) = &ev {
409 term.handle_resize()?;
410 prev_hit_map.clear();
411 prev_scroll_infos.clear();
412 last_mouse_pos = None;
413 }
414 events.push(ev);
415 }
416 }
417 }
418
419 for ev in &events {
420 if let Event::Mouse(mouse) = ev {
421 last_mouse_pos = Some((mouse.x, mouse.y));
422 }
423 }
424 }
425
426 Ok(())
427}
428
429pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
445 run_inline_with(height, RunConfig::default(), f)
446}
447
448pub fn run_inline_with(
453 height: u32,
454 config: RunConfig,
455 mut f: impl FnMut(&mut Context),
456) -> io::Result<()> {
457 install_panic_hook();
458 let mut term = InlineTerminal::new(height, config.mouse)?;
459 let mut events: Vec<Event> = Vec::new();
460 let mut debug_mode: bool = false;
461 let mut tick: u64 = 0;
462 let mut focus_index: usize = 0;
463 let mut prev_focus_count: usize = 0;
464 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
465 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
466 let mut last_mouse_pos: Option<(u32, u32)> = None;
467
468 loop {
469 let (w, h) = term.size();
470 if w == 0 || h == 0 {
471 continue;
472 }
473 let mut ctx = Context::new(
474 std::mem::take(&mut events),
475 w,
476 h,
477 tick,
478 focus_index,
479 prev_focus_count,
480 std::mem::take(&mut prev_scroll_infos),
481 std::mem::take(&mut prev_hit_map),
482 debug_mode,
483 config.theme,
484 last_mouse_pos,
485 );
486 ctx.process_focus_keys();
487
488 f(&mut ctx);
489
490 if ctx.should_quit {
491 break;
492 }
493
494 focus_index = ctx.focus_index;
495 prev_focus_count = ctx.focus_count;
496
497 let mut tree = layout::build_tree(&ctx.commands);
498 let area = crate::rect::Rect::new(0, 0, w, h);
499 layout::compute(&mut tree, area);
500 prev_scroll_infos = layout::collect_scroll_infos(&tree);
501 prev_hit_map = layout::collect_hit_areas(&tree);
502 layout::render(&tree, term.buffer_mut());
503 if debug_mode {
504 layout::render_debug_overlay(&tree, term.buffer_mut());
505 }
506
507 term.flush()?;
508 tick = tick.wrapping_add(1);
509
510 events.clear();
511 if crossterm::event::poll(config.tick_rate)? {
512 let raw = crossterm::event::read()?;
513 if let Some(ev) = event::from_crossterm(raw) {
514 if is_ctrl_c(&ev) {
515 break;
516 }
517 if let Event::Resize(_, _) = &ev {
518 term.handle_resize()?;
519 }
520 events.push(ev);
521 }
522
523 while crossterm::event::poll(Duration::ZERO)? {
524 let raw = crossterm::event::read()?;
525 if let Some(ev) = event::from_crossterm(raw) {
526 if is_ctrl_c(&ev) {
527 return Ok(());
528 }
529 if let Event::Resize(_, _) = &ev {
530 term.handle_resize()?;
531 }
532 events.push(ev);
533 }
534 }
535
536 for ev in &events {
537 if matches!(
538 ev,
539 Event::Key(event::KeyEvent {
540 code: KeyCode::F(12),
541 ..
542 })
543 ) {
544 debug_mode = !debug_mode;
545 }
546 }
547 }
548
549 for ev in &events {
550 if let Event::Mouse(mouse) = ev {
551 last_mouse_pos = Some((mouse.x, mouse.y));
552 }
553 }
554
555 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
556 prev_hit_map.clear();
557 prev_scroll_infos.clear();
558 last_mouse_pos = None;
559 }
560 }
561
562 Ok(())
563}
564
565fn is_ctrl_c(ev: &Event) -> bool {
566 matches!(
567 ev,
568 Event::Key(event::KeyEvent {
569 code: KeyCode::Char('c'),
570 modifiers,
571 }) if modifiers.contains(KeyModifiers::CONTROL)
572 )
573}