systemctl_tui/
app.rs

1use std::{process::Command, sync::Arc};
2
3use anyhow::{Context, Result};
4use log::error;
5use tokio::sync::{mpsc, Mutex};
6use tracing::debug;
7
8use crate::{
9  action::Action,
10  components::{
11    home::{Home, Mode},
12    Component,
13  },
14  event::EventHandler,
15  systemd::{get_all_services, Scope},
16  terminal::TerminalHandler,
17};
18
19pub struct App {
20  pub scope: Scope,
21  pub home: Arc<Mutex<Home>>,
22  pub limit_units: Vec<String>,
23  pub should_quit: bool,
24  pub should_suspend: bool,
25}
26
27impl App {
28  pub fn new(scope: Scope, limit_units: Vec<String>) -> Result<Self> {
29    let home = Home::new(scope, &limit_units);
30    let home = Arc::new(Mutex::new(home));
31    Ok(Self { scope, home, limit_units, should_quit: false, should_suspend: false })
32  }
33
34  pub async fn run(&mut self) -> Result<()> {
35    let (action_tx, mut action_rx) = mpsc::unbounded_channel();
36
37    let (debounce_tx, mut debounce_rx) = mpsc::unbounded_channel();
38
39    let cloned_action_tx = action_tx.clone();
40    tokio::spawn(async move {
41      let debounce_duration = std::time::Duration::from_millis(0);
42      let debouncing = Arc::new(Mutex::new(false));
43
44      loop {
45        let _ = debounce_rx.recv().await;
46
47        if *debouncing.lock().await {
48          continue;
49        }
50
51        *debouncing.lock().await = true;
52
53        let action_tx = cloned_action_tx.clone();
54        let debouncing = debouncing.clone();
55        tokio::spawn(async move {
56          tokio::time::sleep(debounce_duration).await;
57          let _ = action_tx.send(Action::Render);
58          *debouncing.lock().await = false;
59        });
60      }
61    });
62
63    self.home.lock().await.init(action_tx.clone())?;
64
65    let units = get_all_services(self.scope, &self.limit_units)
66      .await
67      .context("Unable to get services. Check that systemd is running and try running this tool with sudo.")?;
68    self.home.lock().await.set_units(units);
69
70    let mut terminal = TerminalHandler::new(self.home.clone());
71    let mut event = EventHandler::new(self.home.clone(), action_tx.clone());
72
73    terminal.render().await;
74
75    loop {
76      if let Some(action) = action_rx.recv().await {
77        match &action {
78          // these are too big to log in full
79          Action::SetLogs { .. } => debug!("action: SetLogs"),
80          Action::SetServices { .. } => debug!("action: SetServices"),
81          _ => debug!("action: {:?}", action),
82        }
83
84        match action {
85          Action::Render => {
86            let start = std::time::Instant::now();
87            terminal.render().await;
88            let duration = start.elapsed();
89            crate::utils::log_perf_event("render", duration);
90          },
91          Action::DebouncedRender => debounce_tx.send(Action::Render).unwrap(),
92          Action::Noop => {},
93          Action::Quit => self.should_quit = true,
94          Action::Suspend => self.should_suspend = true,
95          Action::Resume => self.should_suspend = false,
96          Action::Resize(_, _) => terminal.render().await,
97          // This would normally be in home.rs, but it needs to do some terminal and event handling stuff that's easier here
98          Action::EditUnitFile { unit, path } => {
99            event.stop();
100            let mut tui = terminal.tui.lock().await;
101            tui.exit()?;
102
103            let read_unit_file_contents = || match std::fs::read_to_string(&path) {
104              Ok(contents) => contents,
105              Err(e) => {
106                error!("Failed to read unit file `{}`: {}", path, e);
107                "".to_string()
108              },
109            };
110
111            let unit_file_contents = read_unit_file_contents();
112            let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
113            match Command::new(&editor).arg(&path).status() {
114              Ok(_) => {
115                tui.enter()?;
116                tui.clear()?;
117                event = EventHandler::new(self.home.clone(), action_tx.clone());
118
119                let new_unit_file_contents = read_unit_file_contents();
120                if unit_file_contents != new_unit_file_contents {
121                  action_tx.send(Action::ReloadService(unit))?;
122                }
123
124                action_tx.send(Action::EnterMode(Mode::ServiceList))?;
125              },
126              Err(e) => {
127                tui.enter()?;
128                tui.clear()?;
129                event = EventHandler::new(self.home.clone(), action_tx.clone());
130                action_tx.send(Action::EnterError(format!("Failed to open editor `{}`: {}", editor, e)))?;
131              },
132            }
133          },
134          _ => {
135            if let Some(_action) = self.home.lock().await.dispatch(action) {
136              action_tx.send(_action)?
137            };
138          },
139        }
140      }
141      if self.should_suspend {
142        terminal.suspend()?;
143        event.stop();
144        terminal.task.await?;
145        event.task.await?;
146        terminal = TerminalHandler::new(self.home.clone());
147        event = EventHandler::new(self.home.clone(), action_tx.clone());
148        action_tx.send(Action::Resume)?;
149        action_tx.send(Action::Render)?;
150      } else if self.should_quit {
151        terminal.stop()?;
152        event.stop();
153        terminal.task.await?;
154        event.task.await?;
155        break;
156      }
157    }
158    Ok(())
159  }
160}