tailscale-systray 0.3.0

Application Indicator (SNI) for Tailscale
use std::{fs::File, io::Write, os::unix::ffi::OsStrExt, path::PathBuf};

use anyhow::Context;
use log::{info, warn};
use rust_embed::Embed;

use crate::xdg;

#[derive(Embed)]
#[folder = "icons/"]
struct Icons;

pub fn local_install() -> anyhow::Result<()> {
    install_icons()?;
    install_desktop()?;
    install_autostart()?;

    Ok(())
}

fn install_icons() -> anyhow::Result<()> {
    let prefix = PathBuf::from(std::env::var("HOME").expect("$HOME is not defined"));

    let icons = prefix.join(".local/share/icons/hicolor/scalable/apps");

    std::fs::create_dir_all(&icons)
        .with_context(|| format!("Failed to create directory: {:?}", icons))?;

    for icon in Icons::iter() {
        let icon_relpath = PathBuf::from(icon.to_string());
        if icon_relpath.extension().is_none() {
            continue;
        }
        let icon_file = Icons::get(&icon).unwrap();
        let icon_path = icons.join(icon_relpath.file_name().expect("Icon file has file name"));
        let icon_data = icon_file.data;

        std::fs::write(&icon_path, icon_data)
            .with_context(|| format!("Failed to write icon to: {:?}", icon_path))?;
        info!("Installed icon: {:?}", icon_path);
    }

    let update_cmd = std::process::Command::new("xdg-icon-resource")
        .arg("forceupdate")
        .output();
    if let Ok(output) = update_cmd {
        if output.status.success() {
            info!("Icon cache updated");
        } else {
            warn!("Failed to update icon cache");
        }
    } else {
        warn!("Failed to run xdg-icon-resource");
    }

    Ok(())
}

fn install_desktop() -> anyhow::Result<()> {
    let prefix = xdg::data_home();
    let applications = prefix.join("applications");

    std::fs::create_dir_all(&applications)
        .with_context(|| format!("Failed to create directory: {:?}", applications))?;

    let desktop_file = applications.join("tailscale-systray.desktop");

    let mut file = File::create(&desktop_file)
        .with_context(|| format!("Failed to create desktop file: {:?}", desktop_file))?;

    file.write_all(
        br#"
[Desktop Entry]
Type=Application
Name=Tailscale Systray
Comment=A tailscale indicator (using StatusNotifierItem)
Exec="#,
    )?;

    let exe = std::env::current_exe().with_context(|| "Failed to get current executable path")?;
    file.write_all(exe.as_os_str().as_bytes())?;

    file.write_all(
        br#"
Icon=network-vpn
Terminal=false
Categories=Network;Utility;
StartupNotify=false
"#,
    )?;

    info!("Installed desktop file: {:?}", desktop_file);

    Ok(())
}

fn install_autostart() -> anyhow::Result<()> {
    let autostart_dir = xdg::config_home().join("autostart");
    std::fs::create_dir_all(&autostart_dir)
        .with_context(|| format!("Failed to create directory: {:?}", autostart_dir))?;

    let autostart_file = autostart_dir.join("tailscale-systray.desktop");

    std::os::unix::fs::symlink(
        xdg::data_home().join("applications/tailscale-systray.desktop"),
        &autostart_file,
    )
    .with_context(|| format!("Failed to create symlink: {:?}", autostart_file))?;

    info!("Installed autostart symlink: {:?}", autostart_file);

    Ok(())
}