kael 0.2.0

GPU-accelerated native UI framework for Rust — build desktop apps with Metal, DirectX, and Vulkan rendering
Documentation
use std::path::{Path, PathBuf};

use crate::{DialogKind, DialogOptions, PathPromptOptions};

/// Fallback file-open dialog using zenity/kdialog when XDG Desktop Portal is unavailable.
/// Returns `None` if the user cancelled, or `Some(paths)` on success.
pub fn prompt_for_paths_fallback(options: &PathPromptOptions) -> Option<Vec<PathBuf>> {
    if let Some(result) = try_zenity_open(options) {
        return result;
    }
    if let Some(result) = try_kdialog_open(options) {
        return result;
    }
    // No dialog tool available — return None (cancelled)
    None
}

/// Fallback file-save dialog using zenity/kdialog when XDG Desktop Portal is unavailable.
/// Returns `None` if the user cancelled, or `Some(path)` on success.
pub fn prompt_for_new_path_fallback(
    directory: &Path,
    suggested_name: Option<&str>,
) -> Option<PathBuf> {
    if let Some(result) = try_zenity_save(directory, suggested_name) {
        return result;
    }
    if let Some(result) = try_kdialog_save(directory, suggested_name) {
        return result;
    }
    None
}

fn try_zenity_open(options: &PathPromptOptions) -> Option<Option<Vec<PathBuf>>> {
    let mut cmd = std::process::Command::new("zenity");
    cmd.arg("--file-selection");

    if options.directories {
        cmd.arg("--directory");
    }
    if options.multiple {
        cmd.arg("--multiple");
        cmd.args(["--separator", "\n"]);
    }
    if let Some(ref prompt) = options.prompt {
        cmd.args(["--title", prompt.as_ref()]);
    } else {
        let title = if options.directories {
            "Open Folder"
        } else {
            "Open File"
        };
        cmd.args(["--title", title]);
    }

    let output = cmd.output().ok()?;

    if !output.status.success() {
        // User cancelled
        return Some(None);
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let paths: Vec<PathBuf> = stdout
        .lines()
        .filter(|l| !l.is_empty())
        .map(PathBuf::from)
        .collect();

    if paths.is_empty() {
        Some(None)
    } else {
        Some(Some(paths))
    }
}

fn try_kdialog_open(options: &PathPromptOptions) -> Option<Option<Vec<PathBuf>>> {
    let mut cmd = std::process::Command::new("kdialog");

    if options.directories {
        cmd.arg("--getexistingdirectory");
        cmd.arg(".");
    } else {
        if options.multiple {
            cmd.arg("--getopenfilename");
            cmd.arg(".");
            cmd.arg("*");
            cmd.arg("--multiple");
            cmd.args(["--separate-output"]);
        } else {
            cmd.arg("--getopenfilename");
            cmd.arg(".");
            cmd.arg("*");
        }
    }

    let title = if let Some(ref prompt) = options.prompt {
        prompt.to_string()
    } else if options.directories {
        "Open Folder".to_string()
    } else {
        "Open File".to_string()
    };
    cmd.args(["--title", &title]);

    let output = cmd.output().ok()?;

    if !output.status.success() {
        return Some(None);
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let paths: Vec<PathBuf> = stdout
        .lines()
        .filter(|l| !l.is_empty())
        .map(PathBuf::from)
        .collect();

    if paths.is_empty() {
        Some(None)
    } else {
        Some(Some(paths))
    }
}

fn try_zenity_save(directory: &Path, suggested_name: Option<&str>) -> Option<Option<PathBuf>> {
    let mut cmd = std::process::Command::new("zenity");
    cmd.args(["--file-selection", "--save", "--confirm-overwrite"]);
    cmd.args(["--title", "Save File"]);

    // Build the initial filename path
    if let Some(name) = suggested_name {
        let full_path = directory.join(name);
        cmd.args(["--filename", &full_path.to_string_lossy()]);
    } else {
        let dir_str = format!("{}/", directory.display());
        cmd.args(["--filename", &dir_str]);
    }

    let output = cmd.output().ok()?;

    if !output.status.success() {
        return Some(None);
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let path = stdout.trim();
    if path.is_empty() {
        Some(None)
    } else {
        Some(Some(PathBuf::from(path)))
    }
}

fn try_kdialog_save(directory: &Path, suggested_name: Option<&str>) -> Option<Option<PathBuf>> {
    let mut cmd = std::process::Command::new("kdialog");
    cmd.arg("--getsavefilename");

    if let Some(name) = suggested_name {
        let full_path = directory.join(name);
        cmd.arg(full_path.to_string_lossy().as_ref());
    } else {
        let dir_str = format!("{}/", directory.display());
        cmd.arg(&dir_str);
    }

    cmd.args(["--title", "Save File"]);

    let output = cmd.output().ok()?;

    if !output.status.success() {
        return Some(None);
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let path = stdout.trim();
    if path.is_empty() {
        Some(None)
    } else {
        Some(Some(PathBuf::from(path)))
    }
}

pub fn show_dialog(options: &DialogOptions) -> usize {
    if let Some(idx) = try_zenity(options) {
        return idx;
    }
    if let Some(idx) = try_kdialog(options) {
        return idx;
    }
    0
}

fn try_zenity(options: &DialogOptions) -> Option<usize> {
    let message = build_message(options);
    let button_count = options.buttons.len();

    if button_count <= 1 {
        let zenity_type = match options.kind {
            DialogKind::Error => "--error",
            DialogKind::Warning => "--warning",
            DialogKind::Info => "--info",
        };
        let _ = std::process::Command::new("zenity")
            .args([zenity_type, "--title", &options.title, "--text", &message])
            .output()
            .ok()?;
        return Some(0);
    }

    let mut cmd = std::process::Command::new("zenity");
    cmd.args(["--question", "--title", &options.title, "--text", &message]);

    cmd.args(["--ok-label", &options.buttons[0]]);
    cmd.args(["--cancel-label", &options.buttons[1]]);

    for button in options.buttons.iter().skip(2) {
        cmd.args(["--extra-button", button]);
    }

    let output = cmd.output().ok()?;

    if output.status.success() {
        return Some(0);
    }

    if button_count > 2 {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stdout_trimmed = stdout.trim();
        for (idx, button) in options.buttons.iter().enumerate().skip(2) {
            if stdout_trimmed == button.as_ref() {
                return Some(idx);
            }
        }
    }

    Some(1)
}

fn try_kdialog(options: &DialogOptions) -> Option<usize> {
    let message = build_message(options);
    let button_count = options.buttons.len();

    if button_count <= 1 {
        let kdialog_type = match options.kind {
            DialogKind::Error => "--error",
            DialogKind::Warning => "--sorry",
            DialogKind::Info => "--msgbox",
        };
        let _ = std::process::Command::new("kdialog")
            .args([kdialog_type, &message, "--title", &options.title])
            .output()
            .ok()?;
        return Some(0);
    }

    let mut args = vec!["--warningyesno".to_string(), message];
    if button_count >= 3 {
        args[0] = "--warningyesnocancel".to_string();
    }

    args.push("--title".to_string());
    args.push(options.title.to_string());

    if button_count >= 1 {
        args.push("--yes-label".to_string());
        args.push(options.buttons[0].to_string());
    }
    if button_count >= 2 {
        args.push("--no-label".to_string());
        args.push(options.buttons[1].to_string());
    }
    if button_count >= 3 {
        args.push("--cancel-label".to_string());
        args.push(options.buttons[2].to_string());
    }

    let output = std::process::Command::new("kdialog")
        .args(&args)
        .output()
        .ok()?;

    match output.status.code() {
        Some(0) => Some(0),
        Some(1) => Some(1),
        Some(2) => Some(2),
        _ => Some(0),
    }
}

fn build_message(options: &DialogOptions) -> String {
    match &options.detail {
        Some(detail) if !options.message.is_empty() => {
            format!("{}\n\n{}", options.message, detail)
        }
        Some(detail) => detail.to_string(),
        None => options.message.to_string(),
    }
}