Skip to main content

worktree_io/
daemon.rs

1use anyhow::{bail, Context, Result};
2
3#[derive(Debug, Clone, PartialEq)]
4pub enum DaemonStatus {
5    Installed { path: String },
6    NotInstalled,
7}
8
9impl std::fmt::Display for DaemonStatus {
10    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11        match self {
12            Self::Installed { path } => write!(f, "Installed at {path}"),
13            Self::NotInstalled => write!(f, "Not installed"),
14        }
15    }
16}
17
18pub fn install() -> Result<()> {
19    platform_install()
20}
21
22pub fn uninstall() -> Result<()> {
23    platform_uninstall()
24}
25
26pub fn status() -> Result<DaemonStatus> {
27    platform_status()
28}
29
30// ──────────────────────────── macOS ────────────────────────────
31
32#[cfg(target_os = "macos")]
33fn app_dir() -> std::path::PathBuf {
34    dirs::home_dir()
35        .unwrap_or_else(|| std::path::PathBuf::from("~"))
36        .join("Applications")
37        .join("WorktreeRunner.app")
38}
39
40#[cfg(target_os = "macos")]
41fn platform_install() -> Result<()> {
42    use std::process::Command;
43
44    let exe = std::env::current_exe().context("Failed to get current executable path")?;
45    let app = app_dir();
46    let contents = app.join("Contents");
47    let macos_dir = contents.join("MacOS");
48
49    std::fs::create_dir_all(&macos_dir)
50        .with_context(|| format!("Failed to create {}", macos_dir.display()))?;
51
52    // Shell handler script that forwards the URL as the first argument
53    let handler = macos_dir.join("runner-handler");
54    let script = format!(
55        "#!/bin/sh\nexec {exe} open \"$1\"\n",
56        exe = exe.display()
57    );
58    std::fs::write(&handler, &script)
59        .with_context(|| format!("Failed to write handler script to {}", handler.display()))?;
60
61    // Make it executable
62    Command::new("chmod")
63        .args(["+x"])
64        .arg(&handler)
65        .status()
66        .context("Failed to chmod handler script")?;
67
68    // Info.plist
69    let plist_path = contents.join("Info.plist");
70    let plist = format!(
71        r#"<?xml version="1.0" encoding="UTF-8"?>
72<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
73    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
74<plist version="1.0">
75<dict>
76    <key>CFBundleIdentifier</key>
77    <string>io.worktree.runner</string>
78    <key>CFBundleName</key>
79    <string>WorktreeRunner</string>
80    <key>CFBundleExecutable</key>
81    <string>runner-handler</string>
82    <key>CFBundlePackageType</key>
83    <string>APPL</string>
84    <key>LSUIElement</key>
85    <true/>
86    <key>CFBundleURLTypes</key>
87    <array>
88        <dict>
89            <key>CFBundleURLName</key>
90            <string>Worktree URL</string>
91            <key>CFBundleURLSchemes</key>
92            <array>
93                <string>worktree</string>
94            </array>
95        </dict>
96    </array>
97</dict>
98</plist>
99"#
100    );
101    std::fs::write(&plist_path, plist)
102        .with_context(|| format!("Failed to write Info.plist to {}", plist_path.display()))?;
103
104    // Register with Launch Services
105    let lsregister = "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/\
106        LaunchServices.framework/Versions/A/Support/lsregister";
107    let status = Command::new(lsregister)
108        .arg("-f")
109        .arg(&app)
110        .status()
111        .context("Failed to run lsregister")?;
112
113    if !status.success() {
114        bail!("lsregister failed");
115    }
116
117    println!("Installed WorktreeRunner.app at {}", app.display());
118    println!("The worktree:// URL scheme is now registered.");
119    Ok(())
120}
121
122#[cfg(target_os = "macos")]
123fn platform_uninstall() -> Result<()> {
124    use std::process::Command;
125
126    let app = app_dir();
127    if app.exists() {
128        // Unregister before removing
129        let lsregister = "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/\
130            LaunchServices.framework/Versions/A/Support/lsregister";
131        let _ = Command::new(lsregister)
132            .args(["-u"])
133            .arg(&app)
134            .status();
135
136        std::fs::remove_dir_all(&app)
137            .with_context(|| format!("Failed to remove {}", app.display()))?;
138        println!("Removed {}", app.display());
139    } else {
140        println!("Not installed — nothing to remove.");
141    }
142    Ok(())
143}
144
145#[cfg(target_os = "macos")]
146fn platform_status() -> Result<DaemonStatus> {
147    let app = app_dir();
148    if app.join("Contents").join("Info.plist").exists() {
149        Ok(DaemonStatus::Installed {
150            path: app.display().to_string(),
151        })
152    } else {
153        Ok(DaemonStatus::NotInstalled)
154    }
155}
156
157// ──────────────────────────── Linux ────────────────────────────
158
159#[cfg(target_os = "linux")]
160fn desktop_file() -> std::path::PathBuf {
161    dirs::data_local_dir()
162        .unwrap_or_else(|| std::path::PathBuf::from("~/.local/share"))
163        .join("applications")
164        .join("worktree-runner.desktop")
165}
166
167#[cfg(target_os = "linux")]
168fn platform_install() -> Result<()> {
169    use std::process::Command;
170
171    let exe = std::env::current_exe().context("Failed to get current executable path")?;
172    let path = desktop_file();
173
174    if let Some(parent) = path.parent() {
175        std::fs::create_dir_all(parent)
176            .with_context(|| format!("Failed to create {}", parent.display()))?;
177    }
178
179    let content = format!(
180        "[Desktop Entry]\n\
181         Name=Worktree Runner\n\
182         Exec={exe} open %u\n\
183         Type=Application\n\
184         NoDisplay=true\n\
185         MimeType=x-scheme-handler/worktree;\n",
186        exe = exe.display()
187    );
188    std::fs::write(&path, content)
189        .with_context(|| format!("Failed to write desktop file to {}", path.display()))?;
190
191    Command::new("xdg-mime")
192        .args(["default", "worktree-runner.desktop", "x-scheme-handler/worktree"])
193        .status()
194        .context("Failed to run xdg-mime")?;
195
196    println!("Installed desktop entry at {}", path.display());
197    Ok(())
198}
199
200#[cfg(target_os = "linux")]
201fn platform_uninstall() -> Result<()> {
202    let path = desktop_file();
203    if path.exists() {
204        std::fs::remove_file(&path)
205            .with_context(|| format!("Failed to remove {}", path.display()))?;
206        println!("Removed {}", path.display());
207    } else {
208        println!("Not installed — nothing to remove.");
209    }
210    Ok(())
211}
212
213#[cfg(target_os = "linux")]
214fn platform_status() -> Result<DaemonStatus> {
215    let path = desktop_file();
216    if path.exists() {
217        Ok(DaemonStatus::Installed {
218            path: path.display().to_string(),
219        })
220    } else {
221        Ok(DaemonStatus::NotInstalled)
222    }
223}
224
225// ──────────────────────────── Windows ────────────────────────────
226
227#[cfg(target_os = "windows")]
228fn platform_install() -> Result<()> {
229    use std::process::Command;
230
231    let exe = std::env::current_exe()
232        .context("Failed to get current executable path")?
233        .display()
234        .to_string();
235
236    let run = |args: &[&str]| -> Result<()> {
237        let status = Command::new("reg")
238            .args(args)
239            .status()
240            .context("Failed to run `reg`")?;
241        if !status.success() {
242            bail!("reg command failed");
243        }
244        Ok(())
245    };
246
247    run(&[
248        "add",
249        r"HKCU\Software\Classes\worktree",
250        "/d",
251        "URL:Worktree Protocol",
252        "/f",
253    ])?;
254    run(&[
255        "add",
256        r"HKCU\Software\Classes\worktree",
257        "/v",
258        "URL Protocol",
259        "/d",
260        "",
261        "/f",
262    ])?;
263    run(&[
264        "add",
265        r"HKCU\Software\Classes\worktree\shell\open\command",
266        "/d",
267        &format!(r#""{exe}" open "%1""#),
268        "/f",
269    ])?;
270
271    println!("Registered worktree:// URL scheme in Windows registry.");
272    Ok(())
273}
274
275#[cfg(target_os = "windows")]
276fn platform_uninstall() -> Result<()> {
277    use std::process::Command;
278
279    let status = Command::new("reg")
280        .args(["delete", r"HKCU\Software\Classes\worktree", "/f"])
281        .status()
282        .context("Failed to run `reg delete`")?;
283
284    if !status.success() {
285        bail!("reg delete failed");
286    }
287    println!("Unregistered worktree:// URL scheme.");
288    Ok(())
289}
290
291#[cfg(target_os = "windows")]
292fn platform_status() -> Result<DaemonStatus> {
293    use std::process::Command;
294
295    let output = Command::new("reg")
296        .args(["query", r"HKCU\Software\Classes\worktree"])
297        .output()
298        .context("Failed to query registry")?;
299
300    if output.status.success() {
301        Ok(DaemonStatus::Installed {
302            path: r"HKCU\Software\Classes\worktree".to_string(),
303        })
304    } else {
305        Ok(DaemonStatus::NotInstalled)
306    }
307}
308
309// ──────────────────────────── Fallback ────────────────────────────
310
311#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
312fn platform_install() -> Result<()> {
313    bail!("URL scheme registration is not supported on this platform")
314}
315
316#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
317fn platform_uninstall() -> Result<()> {
318    bail!("URL scheme registration is not supported on this platform")
319}
320
321#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
322fn platform_status() -> Result<DaemonStatus> {
323    Ok(DaemonStatus::NotInstalled)
324}