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 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 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 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}