use crate::config::Config;
use anyhow::{bail, Context, Result};
use colored::Colorize;
use std::io::Write;
use std::path::PathBuf;
const SERVICE_NAME: &str = "mps-notify.service";
const TIMER_NAME: &str = "mps-notify.timer";
const SERVICE_TEMPLATE: &str = "\
[Unit]
Description=mps notification tick
[Service]
Type=oneshot
ExecStart={mps_binary} daemon run
StandardOutput=journal
StandardError=journal
Environment=DISPLAY=:0
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{uid}/bus
";
const TIMER_TEMPLATE: &str = "\
[Unit]
Description=mps notification check (minutely)
[Timer]
OnCalendar=minutely
AccuracySec=30s
Persistent=true
[Install]
WantedBy=timers.target
";
pub fn run(config: &Config, subcommand: &str) -> Result<()> {
match subcommand.to_lowercase().as_str() {
"install" => install(config),
"remove" => remove(),
"status" => status(),
"run" => super::notify::run(config, false, None, false),
other => bail!(
"Unknown daemon subcommand '{}'. Use: install, remove, status, run",
other
),
}
}
fn systemd_user_dir() -> Result<PathBuf> {
let dir = dirs::home_dir()
.context("cannot determine home directory")?
.join(".config/systemd/user");
std::fs::create_dir_all(&dir).with_context(|| format!("cannot create {}", dir.display()))?;
Ok(dir)
}
fn uid() -> String {
if let Ok(s) = std::fs::read_to_string("/proc/self/status") {
for line in s.lines() {
if let Some(rest) = line.strip_prefix("Uid:") {
if let Some(uid_str) = rest.split_whitespace().next() {
return uid_str.to_string();
}
}
}
}
std::env::var("UID").unwrap_or_else(|_| "1000".to_string())
}
fn install(config: &Config) -> Result<()> {
let binary = std::env::current_exe().context("cannot determine current binary path")?;
let binary_str = binary.display().to_string();
if binary_str.contains("/target/debug/") || binary_str.contains("/target/release/") {
println!(
" {} binary is at '{}' (a cargo build path).",
"warn:".yellow(),
binary_str
);
println!(" For a permanent install, use: cargo install mps-rs");
println!(" then re-run: mps daemon install");
println!(" Proceeding anyway...");
}
let uid_str = uid();
let unit_dir = systemd_user_dir()?;
let service_content = SERVICE_TEMPLATE
.replace("{mps_binary}", &binary_str)
.replace("{uid}", &uid_str);
let timer_content = TIMER_TEMPLATE.to_string();
let service_path = unit_dir.join(SERVICE_NAME);
let timer_path = unit_dir.join(TIMER_NAME);
std::fs::write(&service_path, &service_content)
.with_context(|| format!("cannot write {}", service_path.display()))?;
std::fs::write(&timer_path, &timer_content)
.with_context(|| format!("cannot write {}", timer_path.display()))?;
println!(" {} {}", "wrote".green(), service_path.display());
println!(" {} {}", "wrote".green(), timer_path.display());
ensure_gitignored(&config.storage_dir, ".mps.local")?;
sh("systemctl --user daemon-reload")?;
sh("systemctl --user enable --now mps-notify.timer")?;
println!(" {} mps-notify.timer enabled and started", "ok:".green());
println!(" Run {} to check status.", "mps daemon status".bold());
Ok(())
}
fn remove() -> Result<()> {
let _ = sh("systemctl --user disable --now mps-notify.timer");
let unit_dir = systemd_user_dir()?;
for name in &[SERVICE_NAME, TIMER_NAME] {
let p = unit_dir.join(name);
if p.exists() {
std::fs::remove_file(&p).with_context(|| format!("cannot remove {}", p.display()))?;
println!(" {} {}", "removed".green(), p.display());
}
}
sh("systemctl --user daemon-reload")?;
println!(" {} mps-notify.timer removed", "ok:".green());
Ok(())
}
fn status() -> Result<()> {
std::process::Command::new("systemctl")
.args(["--user", "status", "mps-notify.timer"])
.status()
.context("failed to run systemctl")?;
Ok(())
}
fn sh(cmd: &str) -> Result<()> {
let status = std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.status()
.with_context(|| format!("failed to run: {}", cmd))?;
if !status.success() {
bail!("command failed (exit {}): {}", status, cmd);
}
Ok(())
}
fn ensure_gitignored(storage_dir: &std::path::Path, entry: &str) -> Result<()> {
let path = storage_dir.join(".gitignore");
let content = std::fs::read_to_string(&path).unwrap_or_default();
if content.lines().any(|l| l.trim() == entry) {
return Ok(());
}
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("cannot open {}", path.display()))?;
writeln!(f, "{}", entry)?;
println!(" {} {} to {}", "added".green(), entry, path.display());
Ok(())
}