systemctl_tui/
terminal.rs

1use std::{
2  ops::{Deref, DerefMut},
3  sync::Arc,
4};
5
6use anyhow::{anyhow, Context, Result};
7use crossterm::{
8  cursor,
9  event::{DisableMouseCapture, EnableMouseCapture},
10  terminal::{EnterAlternateScreen, LeaveAlternateScreen},
11};
12use ratatui::backend::CrosstermBackend as Backend;
13use signal_hook::{iterator::Signals, low_level};
14use tokio::{
15  sync::{mpsc, Mutex},
16  task::JoinHandle,
17};
18
19use crate::components::{home::Home, Component};
20
21// A struct that mostly exists to be a catch-all for terminal operations that should be synchronized
22pub struct Tui {
23  pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
24}
25
26impl Tui {
27  pub fn new() -> Result<Self> {
28    let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
29
30    // spin up a signal handler to catch SIGTERM and exit gracefully
31    let _ = std::thread::spawn(move || {
32      const SIGNALS: &[libc::c_int] = &[signal_hook::consts::signal::SIGTERM];
33      let mut sigs = Signals::new(SIGNALS).unwrap();
34      let signal = sigs.into_iter().next().unwrap();
35      let _ = exit();
36      low_level::emulate_default_handler(signal).unwrap();
37    });
38
39    Ok(Self { terminal })
40  }
41
42  pub fn enter(&self) -> Result<()> {
43    crossterm::terminal::enable_raw_mode()?;
44    crossterm::execute!(std::io::stderr(), EnterAlternateScreen, EnableMouseCapture, cursor::Hide)?;
45    Ok(())
46  }
47
48  pub fn suspend(&self) -> Result<()> {
49    self.exit()?;
50    #[cfg(not(windows))]
51    signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
52    Ok(())
53  }
54
55  pub fn resume(&self) -> Result<()> {
56    self.enter()?;
57    Ok(())
58  }
59
60  pub fn exit(&self) -> Result<()> {
61    exit()
62  }
63}
64
65// This one's public because we want to expose it to the panic handler
66pub fn exit() -> Result<()> {
67  crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, DisableMouseCapture, cursor::Show)?;
68  crossterm::terminal::disable_raw_mode()?;
69  Ok(())
70}
71
72impl Deref for Tui {
73  type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
74
75  fn deref(&self) -> &Self::Target {
76    &self.terminal
77  }
78}
79
80impl DerefMut for Tui {
81  fn deref_mut(&mut self) -> &mut Self::Target {
82    &mut self.terminal
83  }
84}
85
86impl Drop for Tui {
87  fn drop(&mut self) {
88    exit().unwrap();
89  }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93enum Message {
94  Render,
95  Stop,
96  Suspend,
97}
98
99pub struct TerminalHandler {
100  pub task: JoinHandle<()>,
101  tx: mpsc::UnboundedSender<Message>,
102  home: Arc<Mutex<Home>>,
103  pub tui: Arc<Mutex<Tui>>,
104}
105
106impl TerminalHandler {
107  pub fn new(home: Arc<Mutex<Home>>) -> Self {
108    let (tx, mut rx) = mpsc::unbounded_channel::<Message>();
109    let cloned_home = home.clone();
110    let tui = Tui::new().context(anyhow!("Unable to create terminal")).unwrap();
111    tui.enter().unwrap();
112    let tui = Arc::new(Mutex::new(tui));
113    let cloned_tui = tui.clone();
114    let task = tokio::spawn(async move {
115      loop {
116        match rx.recv().await {
117          Some(Message::Stop) => {
118            exit().unwrap_or_default();
119            break;
120          },
121          Some(Message::Suspend) => {
122            let t = tui.lock().await;
123            t.suspend().unwrap_or_default();
124            break;
125          },
126          Some(Message::Render) => {
127            let mut t = tui.lock().await;
128            let mut home = home.lock().await;
129            render(&mut t, &mut home);
130          },
131          None => {},
132        }
133      }
134    });
135    Self { task, tx, home: cloned_home, tui: cloned_tui }
136  }
137
138  pub fn suspend(&self) -> Result<()> {
139    self.tx.send(Message::Suspend)?;
140    Ok(())
141  }
142
143  pub fn stop(&self) -> Result<()> {
144    self.tx.send(Message::Stop)?;
145    Ok(())
146  }
147
148  pub async fn render(&self) {
149    let mut home = self.home.lock().await;
150    let mut tui = self.tui.lock().await;
151    render(&mut tui, &mut home);
152  }
153
154  // little more performant in situations where we don't need to wait for the render to complete
155  pub fn enqueue_render(&self) -> Result<()> {
156    self.tx.send(Message::Render)?;
157    Ok(())
158  }
159}
160
161fn render(tui: &mut Tui, home: &mut Home) {
162  tui
163    .draw(|f| {
164      home.render(f, f.area());
165    })
166    .expect("Unable to draw");
167}