Skip to content

Commit 9147b51

Browse files
authored
Merge pull request #152 from devmobasa/configurator-daemon-updates
Configurator daemon updates
2 parents c942fc2 + 4005eda commit 9147b51

27 files changed

Lines changed: 2064 additions & 34 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,14 @@ Run wayscriber in the background and toggle with a keybind:
359359
systemctl --user enable --now wayscriber.service
360360
```
361361

362+
No-CLI setup path:
363+
- Open `wayscriber-configurator`
364+
- Go to the `Daemon` tab
365+
- Click `Install/Update Service`, then `Enable + Start`
366+
- Set a shortcut and click `Apply Shortcut`
367+
- GNOME: writes a GNOME custom shortcut (`pkill -SIGUSR1 wayscriber`)
368+
- KDE/Plasma: writes systemd drop-in env (`WAYSCRIBER_PORTAL_SHORTCUT`) for portal global shortcuts
369+
362370
Add keybinding:
363371

364372
Hyprland:
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use std::env;
2+
use std::path::PathBuf;
3+
use std::process::Command;
4+
5+
#[derive(Debug)]
6+
pub(super) struct CommandCapture {
7+
pub(super) success: bool,
8+
pub(super) stdout: String,
9+
pub(super) stderr: String,
10+
}
11+
12+
pub(super) fn command_available(program: &str) -> bool {
13+
find_in_path(program).is_some()
14+
}
15+
16+
pub(super) fn find_in_path(binary_name: &str) -> Option<PathBuf> {
17+
let path_var = env::var_os("PATH")?;
18+
env::split_paths(&path_var)
19+
.map(|directory| directory.join(binary_name))
20+
.find(|path| path.exists())
21+
}
22+
23+
pub(super) fn run_command_checked(program: &str, args: &[&str]) -> Result<CommandCapture, String> {
24+
let capture = run_command(program, args)?;
25+
if capture.success {
26+
return Ok(capture);
27+
}
28+
Err(format_command_failure(program, args, &capture))
29+
}
30+
31+
pub(super) fn run_command(program: &str, args: &[&str]) -> Result<CommandCapture, String> {
32+
let output = Command::new(program).args(args).output().map_err(|err| {
33+
format!(
34+
"Failed to execute `{}` with args [{}]: {}",
35+
program,
36+
args.join(" "),
37+
err
38+
)
39+
})?;
40+
Ok(CommandCapture {
41+
success: output.status.success(),
42+
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
43+
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
44+
})
45+
}
46+
47+
fn format_command_failure(program: &str, args: &[&str], capture: &CommandCapture) -> String {
48+
format!(
49+
"`{}` failed with args [{}]\nstdout: {}\nstderr: {}",
50+
program,
51+
args.join(" "),
52+
capture.stdout.trim(),
53+
capture.stderr.trim()
54+
)
55+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
mod command;
2+
mod service;
3+
mod shortcut;
4+
5+
use crate::models::{
6+
DaemonAction, DaemonActionResult, DaemonRuntimeStatus, DesktopEnvironment, ShortcutBackend,
7+
};
8+
9+
use command::command_available;
10+
use service::{
11+
SERVICE_NAME, detect_service_unit_path, install_or_update_user_service, query_service_active,
12+
query_service_enabled, require_systemctl_available, run_systemctl_user,
13+
};
14+
use shortcut::{apply_shortcut, read_configured_shortcut};
15+
16+
pub(super) async fn load_daemon_runtime_status() -> Result<DaemonRuntimeStatus, String> {
17+
load_daemon_runtime_status_sync()
18+
}
19+
20+
pub(super) async fn perform_daemon_action(
21+
action: DaemonAction,
22+
shortcut_input: String,
23+
) -> Result<DaemonActionResult, String> {
24+
let message = perform_daemon_action_sync(action, shortcut_input.trim())?;
25+
let status = load_daemon_runtime_status_sync()?;
26+
Ok(DaemonActionResult { status, message })
27+
}
28+
29+
fn perform_daemon_action_sync(
30+
action: DaemonAction,
31+
shortcut_input: &str,
32+
) -> Result<String, String> {
33+
match action {
34+
DaemonAction::RefreshStatus => Ok("Daemon status refreshed.".to_string()),
35+
DaemonAction::InstallOrUpdateService => {
36+
let service_path = install_or_update_user_service()?;
37+
Ok(format!(
38+
"Installed/updated user service at {}",
39+
service_path.display()
40+
))
41+
}
42+
DaemonAction::EnableAndStartService => {
43+
require_systemctl_available()?;
44+
run_systemctl_user(&["daemon-reload"])?;
45+
run_systemctl_user(&["enable", "--now", SERVICE_NAME])?;
46+
Ok("Enabled and started wayscriber.service.".to_string())
47+
}
48+
DaemonAction::RestartService => {
49+
require_systemctl_available()?;
50+
run_systemctl_user(&["restart", SERVICE_NAME])?;
51+
Ok("Restarted wayscriber.service.".to_string())
52+
}
53+
DaemonAction::StopAndDisableService => {
54+
require_systemctl_available()?;
55+
run_systemctl_user(&["disable", "--now", SERVICE_NAME])?;
56+
Ok("Stopped and disabled wayscriber.service.".to_string())
57+
}
58+
DaemonAction::ApplyShortcut => apply_shortcut(shortcut_input),
59+
}
60+
}
61+
62+
fn load_daemon_runtime_status_sync() -> Result<DaemonRuntimeStatus, String> {
63+
let desktop = DesktopEnvironment::detect_current();
64+
let systemctl_available = command_available("systemctl");
65+
let gsettings_available = command_available("gsettings");
66+
let shortcut_backend =
67+
ShortcutBackend::from_environment(desktop, gsettings_available, systemctl_available);
68+
let service_unit_path = detect_service_unit_path(systemctl_available);
69+
let service_installed = service_unit_path.is_some();
70+
let service_enabled = if systemctl_available {
71+
query_service_enabled()
72+
} else {
73+
false
74+
};
75+
let service_active = if systemctl_available {
76+
query_service_active()
77+
} else {
78+
false
79+
};
80+
let configured_shortcut = read_configured_shortcut(shortcut_backend);
81+
82+
Ok(DaemonRuntimeStatus {
83+
desktop,
84+
shortcut_backend,
85+
systemctl_available,
86+
gsettings_available,
87+
service_installed,
88+
service_enabled,
89+
service_active,
90+
service_unit_path: service_unit_path.map(|path| path.display().to_string()),
91+
configured_shortcut,
92+
})
93+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use std::env;
2+
use std::fs;
3+
use std::path::{Path, PathBuf};
4+
5+
use wayscriber::systemd_user_service::{
6+
USER_SERVICE_NAME, escape_systemd_env_value as shared_escape_systemd_env_value,
7+
portal_shortcut_dropin_path as shared_portal_shortcut_dropin_path, render_user_service_unit,
8+
user_service_unit_path as shared_user_service_unit_path,
9+
};
10+
11+
use super::command::{command_available, find_in_path, run_command, run_command_checked};
12+
13+
pub(super) const SERVICE_NAME: &str = USER_SERVICE_NAME;
14+
15+
pub(super) fn detect_service_unit_path(systemctl_available: bool) -> Option<PathBuf> {
16+
if systemctl_available {
17+
let capture = run_command(
18+
"systemctl",
19+
&[
20+
"--user",
21+
"show",
22+
"--property=FragmentPath",
23+
"--value",
24+
SERVICE_NAME,
25+
],
26+
)
27+
.ok()?;
28+
if capture.success {
29+
let trimmed = capture.stdout.trim();
30+
if !trimmed.is_empty() && trimmed != "-" {
31+
return Some(PathBuf::from(trimmed));
32+
}
33+
}
34+
}
35+
36+
if let Some(path) = user_service_unit_path()
37+
&& path.exists()
38+
{
39+
return Some(path);
40+
}
41+
42+
package_service_paths()
43+
.into_iter()
44+
.find(|path| path.exists())
45+
}
46+
47+
pub(super) fn query_service_enabled() -> bool {
48+
let capture = match run_command("systemctl", &["--user", "is-enabled", SERVICE_NAME]) {
49+
Ok(capture) => capture,
50+
Err(_) => return false,
51+
};
52+
if !capture.success {
53+
return false;
54+
}
55+
let value = capture.stdout.trim();
56+
matches!(value, "enabled" | "enabled-runtime" | "linked")
57+
}
58+
59+
pub(super) fn query_service_active() -> bool {
60+
let capture = match run_command("systemctl", &["--user", "is-active", SERVICE_NAME]) {
61+
Ok(capture) => capture,
62+
Err(_) => return false,
63+
};
64+
capture.success && capture.stdout.trim() == "active"
65+
}
66+
67+
pub(super) fn require_systemctl_available() -> Result<(), String> {
68+
if command_available("systemctl") {
69+
Ok(())
70+
} else {
71+
Err("systemctl is not available in PATH.".to_string())
72+
}
73+
}
74+
75+
pub(super) fn run_systemctl_user(args: &[&str]) -> Result<(), String> {
76+
let mut full_args = Vec::with_capacity(args.len() + 1);
77+
full_args.push("--user");
78+
full_args.extend_from_slice(args);
79+
let _ = run_command_checked("systemctl", &full_args)?;
80+
Ok(())
81+
}
82+
83+
pub(super) fn user_service_unit_path() -> Option<PathBuf> {
84+
shared_user_service_unit_path()
85+
}
86+
87+
pub(super) fn portal_shortcut_dropin_path() -> Option<PathBuf> {
88+
shared_portal_shortcut_dropin_path()
89+
}
90+
91+
pub(super) fn install_or_update_user_service() -> Result<PathBuf, String> {
92+
let binary_path = resolve_wayscriber_binary_path()?;
93+
94+
let service_path = user_service_unit_path().ok_or_else(|| {
95+
"Cannot resolve home directory; failed to determine user systemd service path.".to_string()
96+
})?;
97+
let service_dir = service_path
98+
.parent()
99+
.ok_or_else(|| "Invalid user service path".to_string())?;
100+
fs::create_dir_all(service_dir).map_err(|err| {
101+
format!(
102+
"Failed to create user service directory {}: {}",
103+
service_dir.display(),
104+
err
105+
)
106+
})?;
107+
108+
let contents = render_user_service_file(&binary_path);
109+
fs::write(&service_path, contents).map_err(|err| {
110+
format!(
111+
"Failed to write user service file {}: {}",
112+
service_path.display(),
113+
err
114+
)
115+
})?;
116+
117+
if command_available("systemctl") {
118+
run_systemctl_user(&["daemon-reload"])?;
119+
}
120+
121+
Ok(service_path)
122+
}
123+
124+
fn package_service_paths() -> Vec<PathBuf> {
125+
vec![
126+
PathBuf::from("/usr/lib/systemd/user").join(SERVICE_NAME),
127+
PathBuf::from("/etc/systemd/user").join(SERVICE_NAME),
128+
PathBuf::from("/lib/systemd/user").join(SERVICE_NAME),
129+
]
130+
}
131+
132+
fn resolve_wayscriber_binary_path() -> Result<PathBuf, String> {
133+
if let Some(path) = env::var_os("WAYSCRIBER_BIN").map(PathBuf::from)
134+
&& path.exists()
135+
{
136+
return Ok(path);
137+
}
138+
139+
if let Ok(current_exe) = env::current_exe()
140+
&& let Some(exe_dir) = current_exe.parent()
141+
{
142+
let sibling = exe_dir.join("wayscriber");
143+
if sibling.exists() {
144+
return Ok(sibling);
145+
}
146+
}
147+
148+
if let Some(path) = find_in_path("wayscriber") {
149+
return Ok(path);
150+
}
151+
152+
Err(
153+
"Unable to locate `wayscriber` binary. Set WAYSCRIBER_BIN or install `wayscriber` in PATH."
154+
.to_string(),
155+
)
156+
}
157+
158+
fn render_user_service_file(binary_path: &Path) -> String {
159+
render_user_service_unit(binary_path)
160+
}
161+
162+
pub(super) fn escape_systemd_env_value(value: &str) -> String {
163+
shared_escape_systemd_env_value(value)
164+
}
165+
166+
#[cfg(test)]
167+
mod tests {
168+
use super::render_user_service_file;
169+
use std::path::Path;
170+
use wayscriber::systemd_user_service::{
171+
portal_shortcut_dropin_path_from_config_root, user_service_unit_path_from_config_root,
172+
};
173+
174+
#[test]
175+
fn service_paths_are_derived_from_xdg_config_root() {
176+
let root = Path::new("/tmp/xdg-config");
177+
assert_eq!(
178+
user_service_unit_path_from_config_root(root),
179+
Path::new("/tmp/xdg-config/systemd/user/wayscriber.service")
180+
);
181+
assert_eq!(
182+
portal_shortcut_dropin_path_from_config_root(root),
183+
Path::new("/tmp/xdg-config/systemd/user/wayscriber.service.d/shortcut.conf")
184+
);
185+
}
186+
187+
#[test]
188+
fn render_user_service_file_quotes_exec_path() {
189+
let unit = render_user_service_file(Path::new("/tmp/My Apps/wayscriber"));
190+
assert!(unit.contains("ExecStart=\"/tmp/My Apps/wayscriber\" --daemon"));
191+
}
192+
}

0 commit comments

Comments
 (0)