aura-gui 0.2.1

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Linux y otros Unix de escritorio: `xdg-open`, `gio trash`, `nice` y Secret Service vía `secret-tool`.

use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};

fn launch(command: &mut Command) -> Result<(), String> {
    let child = command
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .map_err(|error| format!("no se pudo ejecutar xdg-open: {error}"))?;
    crate::core::platform::launch::reap(child)
}

pub fn open_url(url: &str, _background: bool) -> Result<(), String> {
    launch(Command::new("xdg-open").arg(url))
}

/// Sin una forma estándar de seleccionar un archivo: se abre su carpeta, o la propia carpeta.
pub fn reveal(path: &Path) -> Result<(), String> {
    let folder = if path.is_dir() { path } else { path.parent().unwrap_or(path) };
    launch(Command::new("xdg-open").arg(folder))
}

/// `gio trash` (GLib, presente en casi todo escritorio) y, si falta, `trash-put` de trash-cli.
pub fn trash(path: &Path) -> Result<(), String> {
    let run = |mut command: Command| {
        command.arg(path).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()).status()
    };
    let mut gio = Command::new("gio");
    gio.arg("trash");
    match run(gio) {
        Ok(status) if status.success() => Ok(()),
        Ok(status) => Err(format!("gio respondió {status}")),
        Err(_) => match run(Command::new("trash-put")) {
            Ok(status) if status.success() => Ok(()),
            _ => Err("instala gio (GLib) o trash-cli".into()),
        },
    }
}

pub fn low_priority_command(program: &str) -> Command {
    if !Path::new("/usr/bin/nice").is_file() {
        return Command::new(program);
    }
    let mut command = Command::new("/usr/bin/nice");
    command.args(["-n", "10", program]);
    command
}

const NO_STORE: &str = "no hay Secret Service: instala secret-tool (libsecret-tools)";

/// El valor viaja por la entrada estándar de `secret-tool`, no como argumento (visible con `ps`).
pub fn secret_set(service: &str, account: &str, value: &str) -> Result<(), String> {
    let mut child = Command::new("secret-tool")
        .args(["store", "--label", service, "service", service, "account", account])
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .map_err(|_| NO_STORE.to_owned())?;
    let written = child.stdin.take().ok_or("secret-tool no abrió su entrada")?.write_all(value.as_bytes());
    let status = child.wait().map_err(|error| format!("secret-tool no terminó: {error}"))?;
    match (written, status.success()) {
        (Ok(()), true) => Ok(()),
        _ => Err(format!("secret-tool no guardó el valor ({status}); ¿hay un servicio de llaveros activo?")),
    }
}

pub fn secret_get(service: &str, account: &str) -> Option<String> {
    let output = Command::new("secret-tool")
        .args(["lookup", "service", service, "account", account])
        .stdin(Stdio::null())
        .stderr(Stdio::null())
        .output()
        .ok()?;
    output.status.success().then(|| String::from_utf8_lossy(&output.stdout).into_owned())
}

pub fn secret_remove(service: &str, account: &str) -> Result<(), String> {
    // `clear` sale con 0 aunque no haya nada que borrar; si falta la herramienta no hay nada guardado.
    let _ = Command::new("secret-tool")
        .args(["clear", "service", service, "account", account])
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status();
    Ok(())
}