systemctl_tui/
terminal.rs1use 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
20pub 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 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
64pub 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 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}