Skip to content

Commit 38f6942

Browse files
authored
Merge pull request #28 from rgwood/edit-unit-file
Add functionality for editing unit file
2 parents 1e5fcb9 + ee36e82 commit 38f6942

7 files changed

Lines changed: 77 additions & 47 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "systemctl-tui"
33
description = "A simple TUI for interacting with systemd services and their logs"
44
homepage = "https://github.com/rgwood/systemctl-tui"
55
repository = "https://github.com/rgwood/systemctl-tui"
6-
version = "0.3.8"
6+
version = "0.3.9"
77
edition = "2021"
88
authors = ["Reilly Wood"]
99
license = "MIT"

src/action.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ pub enum Action {
1616
RefreshServices,
1717
SetServices(Vec<UnitWithStatus>),
1818
EnterMode(Mode),
19-
EnterError { err: String },
19+
EnterError(String),
2020
CancelTask,
2121
ToggleHelp,
22-
SetUnitFilePath { unit: UnitId, path: String },
22+
SetUnitFilePath { unit: UnitId, path: Result<String, String> },
2323
CopyUnitFilePath,
2424
SetLogs { unit: UnitId, logs: Vec<String> },
2525
AppendLogLine { unit: UnitId, line: String },
@@ -33,5 +33,6 @@ pub enum Action {
3333
ScrollDown(u16),
3434
ScrollToTop,
3535
ScrollToBottom,
36+
EditUnitFile { path: String },
3637
Noop,
3738
}

src/app.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
use std::sync::Arc;
1+
use std::{process::Command, sync::Arc};
22

33
use anyhow::{Context, Result};
44
use tokio::sync::{mpsc, Mutex};
55
use tracing::debug;
66

77
use crate::{
88
action::Action,
9-
components::{home::Home, Component},
9+
components::{
10+
home::{Home, Mode},
11+
Component,
12+
},
1013
event::EventHandler,
1114
systemd::{get_all_services, Scope},
1215
terminal::TerminalHandler,
@@ -90,6 +93,23 @@ impl App {
9093
Action::Suspend => self.should_suspend = true,
9194
Action::Resume => self.should_suspend = false,
9295
Action::Resize(_, _) => terminal.render().await,
96+
Action::EditUnitFile { path } => {
97+
event.stop();
98+
let mut tui = terminal.tui.lock().await;
99+
tui.exit()?;
100+
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
101+
match Command::new(editor).arg(path).status() {
102+
Ok(_) => {
103+
tui.enter()?;
104+
tui.clear()?;
105+
event = EventHandler::new(self.home.clone(), action_tx.clone());
106+
action_tx.send(Action::EnterMode(Mode::ServiceList))?;
107+
},
108+
Err(e) => {
109+
action_tx.send(Action::EnterError(format!("Failed to open editor: {}", e)))?;
110+
},
111+
}
112+
},
93113
_ => {
94114
if let Some(_action) = self.home.lock().await.dispatch(action) {
95115
action_tx.send(_action)?

src/components/home.rs

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ impl Home {
316316
error_string.push_str("Try running this tool with sudo.");
317317
}
318318

319-
tx.send(Action::EnterError { err: error_string }).unwrap();
319+
tx.send(Action::EnterError(error_string)).unwrap();
320320
},
321321
}
322322
spinner_task.abort();
@@ -366,11 +366,13 @@ impl Component for Home {
366366
// get the unit file path
367367
match systemd::get_unit_file_location(&unit) {
368368
Ok(path) => {
369-
let _ = tx.send(Action::SetUnitFilePath { unit: unit.clone(), path });
369+
let _ = tx.send(Action::SetUnitFilePath { unit: unit.clone(), path: Ok(path) });
370370
let _ = tx.send(Action::Render);
371371
},
372372
Err(e) => {
373-
let _ = tx.send(Action::SetUnitFilePath { unit: unit.clone(), path: "(could not be determined)".into() });
373+
// Fix this!!! Set the path to an error enum variant instead of a string
374+
let _ =
375+
tx.send(Action::SetUnitFilePath { unit: unit.clone(), path: Err("could not be determined".into()) });
374376
let _ = tx.send(Action::Render);
375377
error!("Error getting unit file path for {}: {}", unit.name, e);
376378
},
@@ -557,31 +559,34 @@ impl Component for Home {
557559
},
558560
Action::EnterMode(mode) => {
559561
if mode == Mode::ActionMenu {
560-
let selected = match self.filtered_units.selected() {
561-
Some(s) => s.id(),
562-
None => return None,
563-
};
564-
565-
// TODO: use current status to determine which actions are available?
566-
let menu_items = vec![
567-
MenuItem::new("Start", Action::StartService(selected.clone())),
568-
MenuItem::new("Stop", Action::StopService(selected.clone())),
569-
MenuItem::new("Restart", Action::RestartService(selected.clone())),
570-
MenuItem::new("Copy unit file path to clipboard", Action::CopyUnitFilePath),
571-
// TODO add these
572-
// MenuItem::new("Reload", Action::ReloadService(selected.clone())),
573-
// MenuItem::new("Enable", Action::EnableService(selected.clone())),
574-
// MenuItem::new("Disable", Action::DisableService(selected.clone())),
575-
];
576-
577-
self.menu_items = StatefulList::with_items(menu_items);
578-
self.menu_items.state.select(Some(0));
562+
if let Some(selected) = self.filtered_units.selected() {
563+
let mut menu_items = vec![
564+
MenuItem::new("Start", Action::StartService(selected.id())),
565+
MenuItem::new("Stop", Action::StopService(selected.id())),
566+
MenuItem::new("Restart", Action::RestartService(selected.id())),
567+
// TODO add these
568+
// MenuItem::new("Reload", Action::ReloadService(selected.clone())),
569+
// MenuItem::new("Enable", Action::EnableService(selected.clone())),
570+
// MenuItem::new("Disable", Action::DisableService(selected.clone())),
571+
];
572+
573+
if let Some(Ok(file_path)) = &selected.file_path {
574+
menu_items.push(MenuItem::new("Copy unit file path to clipboard", Action::CopyUnitFilePath));
575+
menu_items.push(MenuItem::new("Edit unit file", Action::EditUnitFile { path: file_path.clone() }));
576+
}
577+
578+
self.menu_items = StatefulList::with_items(menu_items);
579+
self.menu_items.state.select(Some(0));
580+
} else {
581+
return None;
582+
}
579583
}
580584

581585
self.mode = mode;
582586
return Some(Action::Render);
583587
},
584-
Action::EnterError { err } => {
588+
Action::EnterError(err) => {
589+
tracing::error!(err);
585590
self.error_message = err;
586591
return Some(Action::EnterMode(Mode::Error));
587592
},
@@ -596,13 +601,13 @@ impl Component for Home {
596601
},
597602
Action::CopyUnitFilePath => {
598603
if let Some(selected) = self.filtered_units.selected() {
599-
if let Some(file_path) = &selected.file_path {
604+
if let Some(Ok(file_path)) = &selected.file_path {
600605
match clipboard_anywhere::set_clipboard(file_path) {
601606
Ok(_) => return Some(Action::EnterMode(Mode::ServiceList)),
602-
Err(e) => return Some(Action::EnterError { err: format!("Error copying to clipboard: {}", e) }),
607+
Err(e) => return Some(Action::EnterError(format!("Error copying to clipboard: {}", e))),
603608
}
604609
} else {
605-
return Some(Action::EnterError { err: "No unit file path available".into() });
610+
return Some(Action::EnterError("No unit file path available".into()));
606611
}
607612
}
608613
},
@@ -794,17 +799,18 @@ impl Component for Home {
794799
UnitScope::User => "User",
795800
};
796801

797-
let mut lines = vec![
802+
let lines = vec![
798803
colored_line(&i.description, Color::Reset),
799804
colored_line(scope, Color::Reset),
800805
colored_line(&i.load_state, load_color),
801806
line_color_string(active_state_value, active_color),
807+
match &i.file_path {
808+
Some(Ok(file_path)) => Line::from(file_path.as_str()),
809+
Some(Err(e)) => colored_line(e, Color::Red),
810+
None => Line::from(""),
811+
},
802812
];
803813

804-
if let Some(file_path) = &i.file_path {
805-
lines.push(Line::from(file_path.as_str()));
806-
}
807-
808814
lines
809815
} else {
810816
vec![]

src/systemd.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use zbus::{proxy, zvariant, Connection};
1111

1212
#[derive(Debug, Clone)]
1313
pub struct UnitWithStatus {
14-
pub name: String, // The primary unit name as string
15-
pub scope: UnitScope, // System or user?
16-
pub description: String, // The human readable description string
17-
pub file_path: Option<String>, // The unit file path - populated later on demand
14+
pub name: String, // The primary unit name as string
15+
pub scope: UnitScope, // System or user?
16+
pub description: String, // The human readable description string
17+
pub file_path: Option<Result<String, String>>, // The unit file path - populated later on demand
1818

1919
pub load_state: String, // The load state (i.e. whether the unit file has been loaded successfully)
2020

@@ -27,7 +27,6 @@ pub struct UnitWithStatus {
2727
/// The other state all units have is called the "enablement state". It describes how the unit might be automatically started in the future. A unit is enabled if it has been added to the requirements list of any other unit though symlinks in the filesystem. The set of symlinks to be created when enabling a unit is described by the unit's [Install] section. A unit is disabled if no symlinks are present. Again there's a variety of other values other than these two (e.g. not all units even have [Install] sections).
2828
/// Only populated when needed b/c this is much slower to get
2929
pub enablement_state: Option<String>,
30-
3130
// We don't use any of these right now, might as well skip'em so there's less data to clone
3231
// pub followed: String, // A unit that is being followed in its state by this unit, if there is any, otherwise the empty string.
3332
// pub path: String, // The unit object path

src/terminal.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ use tokio::{
1818

1919
use crate::components::{home::Home, Component};
2020

21-
// pub type Frame<'a> = ratatui::Frame<'a, Backend<std::io::Stderr>>;
22-
21+
// A struct that mostly exists to be a catch-all for terminal operations that should be synchronized
2322
pub struct Tui {
2423
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
2524
}
@@ -47,7 +46,7 @@ impl Tui {
4746
}
4847

4948
pub fn suspend(&self) -> Result<()> {
50-
exit()?;
49+
self.exit()?;
5150
#[cfg(not(windows))]
5251
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
5352
Ok(())
@@ -57,8 +56,13 @@ impl Tui {
5756
self.enter()?;
5857
Ok(())
5958
}
59+
60+
pub fn exit(&self) -> Result<()> {
61+
exit()
62+
}
6063
}
6164

65+
// This one's public because we want to expose it to the panic handler
6266
pub fn exit() -> Result<()> {
6367
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, DisableMouseCapture, cursor::Show)?;
6468
crossterm::terminal::disable_raw_mode()?;
@@ -96,7 +100,7 @@ pub struct TerminalHandler {
96100
pub task: JoinHandle<()>,
97101
tx: mpsc::UnboundedSender<Message>,
98102
home: Arc<Mutex<Home>>,
99-
tui: Arc<Mutex<Tui>>,
103+
pub tui: Arc<Mutex<Tui>>,
100104
}
101105

102106
impl TerminalHandler {

0 commit comments

Comments
 (0)