Skip to main content

any_dialog/backends/
mod.rs

1//! The backends: one file per helper program.
2//!
3//! Everything specific to a particular helper lives in that helper's
4//! file: its program name and how it wants each dialog said. Shared
5//! things stay shared and live elsewhere: what a dialog is
6//! ([`crate::request`]), what counts as an answer or a cancel
7//! ([`crate::outcome`]), and the error type ([`crate::error`]). A
8//! backend whose answer convention differs from the shared one
9//! brings its own classifier in its own file: osascript is the
10//! standing example.
11//!
12//! [`Backend`] and the probing that finds a helper also live here,
13//! since the set of backends and the probe order are facts about the
14//! backends as a group.
15
16mod kdialog;
17mod osascript;
18mod powershell;
19mod zenity;
20
21use std::path::{Path, PathBuf};
22
23use crate::outcome::Outcome;
24use crate::request::Request;
25
26/// Which helper program a [`crate::Dialogs`] handle drives.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum Backend {
29    Kdialog,
30    Zenity,
31    Osascript,
32    Powershell,
33}
34
35/// Probe for a backend, in preference order. Each OS's bundled
36/// helper goes first on its own OS (osascript on macOS,
37/// powershell.exe on Windows); anywhere else the bundled helpers
38/// are last resorts that normally are not installed at all.
39pub(crate) fn probe() -> Option<(Backend, PathBuf)> {
40    let order: [(Backend, &str); 4] = if cfg!(target_os = "windows") {
41        [
42            (Backend::Powershell, powershell::PROGRAM),
43            (Backend::Kdialog, kdialog::PROGRAM),
44            (Backend::Zenity, zenity::PROGRAM),
45            (Backend::Osascript, osascript::PROGRAM),
46        ]
47    } else if cfg!(target_os = "macos") {
48        [
49            (Backend::Osascript, osascript::PROGRAM),
50            (Backend::Kdialog, kdialog::PROGRAM),
51            (Backend::Zenity, zenity::PROGRAM),
52            (Backend::Powershell, powershell::PROGRAM),
53        ]
54    } else {
55        [
56            (Backend::Kdialog, kdialog::PROGRAM),
57            (Backend::Zenity, zenity::PROGRAM),
58            (Backend::Osascript, osascript::PROGRAM),
59            (Backend::Powershell, powershell::PROGRAM),
60        ]
61    };
62    for (backend, program) in order {
63        if let Some(path) = find_on_path(program) {
64            return Some((backend, path));
65        }
66    }
67    None
68}
69
70/// Build the argument list for `backend`, excluding argv[0].
71pub(crate) fn argv(backend: Backend, request: &Request<'_>) -> Vec<String> {
72    match backend {
73        Backend::Kdialog => kdialog::argv(request),
74        Backend::Zenity => zenity::argv(request),
75        Backend::Osascript => osascript::argv(request),
76        Backend::Powershell => powershell::argv(request),
77    }
78}
79
80/// Environment variables to set for a run. Only the PowerShell
81/// backend takes its values this way (see its module docs); the
82/// other backends take everything as arguments.
83pub(crate) fn env(backend: Backend, request: &Request<'_>) -> Vec<(String, String)> {
84    match backend {
85        Backend::Powershell => powershell::env(request),
86        _ => Vec::new(),
87    }
88}
89
90/// Interpret what a helper said on its way out. Backends using the
91/// shared convention go through [`crate::outcome`]; osascript and
92/// PowerShell exit 1 for genuine errors too, so they classify for
93/// themselves.
94pub(crate) fn classify(
95    backend: Backend,
96    exit_code: Option<i32>,
97    stdout: &str,
98    stderr: &str,
99) -> Outcome {
100    match backend {
101        Backend::Osascript => osascript::classify(exit_code, stdout, stderr),
102        Backend::Powershell => powershell::classify(exit_code, stdout, stderr),
103        _ => crate::outcome::classify(exit_code, stdout),
104    }
105}
106
107/// The choice a list dialog should start on: the requested current
108/// choice when it is among the choices, else the first choice. Shared
109/// by the backends whose list dialogs take a preselection.
110pub(crate) fn selected<'a>(choices: &[&'a str], current: Option<&'a str>) -> Option<&'a str> {
111    current
112        .filter(|current| choices.contains(current))
113        .or_else(|| choices.first().copied())
114}
115
116/// The choices a multiple-choice dialog should start with: the
117/// requested ones that are actually among the choices.
118pub(crate) fn selected_many<'a>(choices: &[&'a str], current: &[&'a str]) -> Vec<&'a str> {
119    current
120        .iter()
121        .filter(|choice| choices.contains(choice))
122        .copied()
123        .collect()
124}
125
126/// Decode a multiple-choice answer into the picked choices.
127/// Every backend but kdialog prints one pick per line; kdialog's
128/// checklist prints index tags instead (see its file), so it
129/// decodes for itself.
130pub(crate) fn decode_choices(backend: Backend, text: &str, choices: &[&str]) -> Vec<String> {
131    match backend {
132        Backend::Kdialog => kdialog::decode_choices(text, choices),
133        _ => text.lines().map(|line| line.to_string()).collect(),
134    }
135}
136
137/// A path as the text a helper program wants. Non-UTF-8 paths
138/// degrade lossily, which is the best a spawned helper can be
139/// handed through argv or the environment anyway.
140pub(crate) fn path_text(path: &Path) -> String {
141    path.to_string_lossy().into_owned()
142}
143
144/// Find an executable on `PATH`.
145pub(crate) fn find_on_path(program: &str) -> Option<PathBuf> {
146    let path = std::env::var_os("PATH")?;
147    std::env::split_paths(&path)
148        .map(|dir| dir.join(program))
149        .find(|candidate| is_executable(candidate))
150}
151
152fn is_executable(path: &Path) -> bool {
153    let Ok(metadata) = std::fs::metadata(path) else {
154        return false;
155    };
156    if !metadata.is_file() {
157        return false;
158    }
159    #[cfg(unix)]
160    {
161        use std::os::unix::fs::PermissionsExt;
162        metadata.permissions().mode() & 0o111 != 0
163    }
164    #[cfg(not(unix))]
165    {
166        true
167    }
168}