Skip to main content

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    // Fetch unit files (includes enablement state and disabled units not returned by ListUnits)
71    action_tx.send(Action::RefreshUnitFiles)?;
72
73    let mut terminal = TerminalHandler::new(self.home.clone());
74    let mut event = EventHandler::new(self.home.clone(), action_tx.clone());
75
76    terminal.render().await;
77
78    loop {
79      if let Some(action) = action_rx.recv().await {
80        match &action {
81          // these are too big to log in full
82          Action::SetLogs { .. } => debug!("action: SetLogs"),
83          Action::SetServices { .. } => debug!("action: SetServices"),
84          _ => debug!("action: {:?}", action),
85        }
86
87        match action {
88          Action::Render => terminal.render().await,
89          Action::DebouncedRender => debounce_tx.send(Action::Render).unwrap(),
90          Action::Noop => {},
91          Action::Quit => self.should_quit = true,
92          Action::Suspend => self.should_suspend = true,
93          Action::Resume => self.should_suspend = false,
94          Action::Resize(_, _) => terminal.render().await,
95          // This would normally be in home.rs, but it needs to do some terminal and event handling stuff that's easier here
96          Action::EditUnitFile { unit, path } => {
97            event.stop();
98            let mut tui = terminal.tui.lock().await;
99            tui.exit()?;
100
101            let read_unit_file_contents = || match std::fs::read_to_string(&path) {
102              Ok(contents) => contents,
103              Err(e) => {
104                error!("Failed to read unit file `{path}`: {e}");
105                "".to_string()
106              },
107            };
108
109            let unit_file_contents = read_unit_file_contents();
110            let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
111            match Command::new(&editor).arg(&path).status() {
112              Ok(_) => {
113                tui.enter()?;
114                tui.clear()?;
115                event = EventHandler::new(self.home.clone(), action_tx.clone());
116
117                let new_unit_file_contents = read_unit_file_contents();
118                if unit_file_contents != new_unit_file_contents {
119                  action_tx.send(Action::ReloadService(unit))?;
120                }
121
122                action_tx.send(Action::EnterMode(Mode::ServiceList))?;
123              },
124              Err(e) => {
125                tui.enter()?;
126                tui.clear()?;
127                event = EventHandler::new(self.home.clone(), action_tx.clone());
128                action_tx.send(Action::EnterError(format!("Failed to open editor `{editor}`: {e}")))?;
129              },
130            }
131          },
132          Action::OpenLogsInPager { logs } => {
133            event.stop();
134            let mut tui = terminal.tui.lock().await;
135            tui.exit()?;
136
137            let temp_path = std::env::temp_dir().join("systemctl-tui-logs.txt");
138            if let Err(e) = std::fs::write(&temp_path, logs.join("\n")) {
139              tui.enter()?;
140              tui.clear()?;
141              event = EventHandler::new(self.home.clone(), action_tx.clone());
142              action_tx.send(Action::EnterError(format!("Failed to write temp file: {e}")))?;
143            } else {
144              let pager = std::env::var("PAGER").unwrap_or_else(|_| "less".to_string());
145              match Command::new(&pager).arg(&temp_path).status() {
146                Ok(_) => {
147                  tui.enter()?;
148                  tui.clear()?;
149                  event = EventHandler::new(self.home.clone(), action_tx.clone());
150                  action_tx.send(Action::EnterMode(Mode::ServiceList))?;
151                },
152                Err(e) => {
153                  tui.enter()?;
154                  tui.clear()?;
155                  event = EventHandler::new(self.home.clone(), action_tx.clone());
156                  action_tx.send(Action::EnterError(format!("Failed to open pager `{pager}`: {e}")))?;
157                },
158              }
159              let _ = std::fs::remove_file(&temp_path);
160            }
161          },
162          _ => {
163            if let Some(_action) = self.home.lock().await.dispatch(action) {
164              action_tx.send(_action)?
165            };
166          },
167        }
168      }
169      if self.should_suspend {
170        terminal.suspend()?;
171        event.stop();
172        terminal.task.await?;
173        event.task.await?;
174        terminal = TerminalHandler::new(self.home.clone());
175        event = EventHandler::new(self.home.clone(), action_tx.clone());
176        action_tx.send(Action::Resume)?;
177        action_tx.send(Action::Render)?;
178      } else if self.should_quit {
179        terminal.stop()?;
180        event.stop();
181        terminal.task.await?;
182        event.task.await?;
183        break;
184      }
185    }
186    Ok(())
187  }
188}