Skip to main content

rustic_rs/commands/
tui.rs

1//! `tui` subcommand
2mod diff;
3mod ls;
4mod progress;
5mod restore;
6mod snapshots;
7pub mod summary;
8mod tree;
9mod widgets;
10
11pub use diff::Diff;
12pub use ls::Ls;
13pub use snapshots::Snapshots;
14
15use std::io;
16use std::sync::{Arc, RwLock};
17
18use anyhow::Result;
19use crossterm::event::{KeyEvent, KeyModifiers};
20use crossterm::{
21    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
22    execute,
23    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
24};
25use progress::TuiProgressBars;
26use ratatui::prelude::*;
27use scopeguard::defer;
28use widgets::{Draw, ProcessEvent};
29
30pub trait TuiResult {
31    fn exit(&self) -> bool;
32}
33
34impl TuiResult for bool {
35    fn exit(&self) -> bool {
36        *self
37    }
38}
39
40pub fn run(f: impl FnOnce(TuiProgressBars) -> Result<()>) -> Result<()> {
41    // setup terminal
42    let terminal = init_terminal()?;
43    let terminal = Arc::new(RwLock::new(terminal));
44
45    // restore terminal (even when leaving through ?, early return, or panic)
46    defer! {
47        reset_terminal().unwrap();
48    }
49
50    let progress = TuiProgressBars { terminal };
51
52    if let Err(err) = f(progress) {
53        println!("{err:?}");
54    }
55
56    Ok(())
57}
58
59/// Initializes the terminal.
60fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
61    execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
62    enable_raw_mode()?;
63
64    let backend = CrosstermBackend::new(io::stdout());
65
66    let mut terminal = Terminal::new(backend)?;
67    terminal.hide_cursor()?;
68
69    Ok(terminal)
70}
71
72/// Resets the terminal.
73fn reset_terminal() -> Result<()> {
74    disable_raw_mode()?;
75    execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
76    Ok(())
77}
78
79pub fn run_app<B: Backend, A, T>(terminal: Arc<RwLock<Terminal<B>>>, mut app: A) -> Result<()>
80where
81    A: Draw + ProcessEvent<Result = Result<T>>,
82    T: TuiResult,
83    B::Error: Send + Sync + 'static,
84{
85    loop {
86        _ = terminal.write().unwrap().draw(|f| ui(f, &mut app))?;
87        let event = event::read()?;
88
89        if let Event::Key(KeyEvent {
90            code: KeyCode::Char('c'),
91            modifiers: KeyModifiers::CONTROL,
92            kind: KeyEventKind::Press,
93            ..
94        }) = event
95        {
96            return Ok(());
97        }
98        if app.input(event)?.exit() {
99            return Ok(());
100        }
101    }
102}
103
104fn ui<A: Draw>(f: &mut Frame<'_>, app: &mut A) {
105    let area = f.area();
106    app.draw(area, f);
107}