Skip to main content

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