diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/backends/kdialog.rs | 143 | ||||
-rw-r--r-- | src/backends/mod.rs | 3 | ||||
-rw-r--r-- | src/lib.rs | 24 |
3 files changed, 167 insertions, 3 deletions
diff --git a/src/backends/kdialog.rs b/src/backends/kdialog.rs new file mode 100644 index 0000000..153e4c5 --- /dev/null +++ b/src/backends/kdialog.rs @@ -0,0 +1,143 @@ +// Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org> +// Copyright (C) 2019 Stephan Sokolow <http://www.ssokolow.com/ContactMe> +// SPDX-License-Identifier: MIT + +use std::process; + +use crate::{Choice, Error, Input, Message, Password, Question, Result}; + +/// Subprocess exit codes +/// +/// Note: `kdialog` doesn't have a fixed correspondence between button labels and status codes. +/// The following mappings occur: +/// +/// - Yes/No = `0`/`1` +/// - Yes/No/Cancel = `0`/`1`/`2` +/// - OK/Cancel = `0`/`1` +const OK: i32 = 0; +const CANCEL: i32 = 1; + +/// The `kdialog` backend. +/// +/// This backend uses the external `kdialog` program to display KDE dialog boxes. +#[derive(Debug)] +pub struct KDialog { + icon: Option<String>, + // TODO: --dontagain +} + +impl KDialog { + /// Creates a new `KDialog` instance without configuration. + pub fn new() -> KDialog { + KDialog { icon: None } + } + + /// Sets the icon in the dialog box's titlebar and taskbar button. + /// + /// The icon can be either a name from the user's configured icon theme, such as `error` or + /// `info` or the path to an image to use. + /// + /// The default image depends on the dialog type. + pub fn set_icon(&mut self, icon: impl Into<String>) { + self.icon = Some(icon.into()); + } + + pub(crate) fn is_available() -> bool { + super::is_available("kdialog") + } + + fn execute(&self, args: Vec<&str>, title: &Option<String>) -> Result<process::Output> { + let mut command = process::Command::new("kdialog"); + + if let Some(ref icon) = self.icon { + command.arg("--icon"); + command.arg(icon); + } + if let Some(ref title) = title { + command.arg("--title"); + command.arg(title); + } + + command.args(args); + command.output().map_err(Error::IoError) + } +} + +impl AsRef<KDialog> for KDialog { + fn as_ref(&self) -> &Self { + self + } +} + +fn require_success(status: process::ExitStatus) -> Result<()> { + if status.success() { + Ok(()) + } else { + if let Some(code) = status.code() { + match code { + CANCEL => Ok(()), + _ => Err(Error::from(("kdialog", status))), + } + } else { + Err(Error::from(("kdialog", status))) + } + } +} + +fn get_choice(status: process::ExitStatus) -> Result<Choice> { + if let Some(code) = status.code() { + match code { + OK => Ok(Choice::Yes), + CANCEL => Ok(Choice::No), + _ => Err(Error::from(("kdialog", status))), + } + } else { + Err(Error::from(("kdialog", status))) + } +} + +fn get_stdout(output: process::Output) -> Result<Option<String>> { + if output.status.success() { + String::from_utf8(output.stdout) + .map(|s| Some(s.trim_end_matches('\n').to_string())) + .map_err(|err| Error::from(err)) + } else { + if let Some(code) = output.status.code() { + match code { + OK => Ok(None), + CANCEL => Ok(None), + _ => Err(Error::from(("kdialog", output.status))), + } + } else { + Err(Error::from(("kdialog", output.status))) + } + } +} + +impl super::Backend for KDialog { + fn show_input(&self, input: &Input) -> Result<Option<String>> { + let mut args = vec!["--inputbox", &input.text]; + if let Some(ref default) = input.default { + args.push(default); + } + self.execute(args, &input.title).and_then(get_stdout) + } + + fn show_message(&self, message: &Message) -> Result<()> { + let args = vec!["--msgbox", &message.text]; + self.execute(args, &message.title) + .and_then(|output| require_success(output.status)) + .map(|_| ()) + } + + fn show_password(&self, password: &Password) -> Result<Option<String>> { + let args = vec!["--password", &password.text]; + self.execute(args, &password.title).and_then(get_stdout) + } + + fn show_question(&self, question: &Question) -> Result<Choice> { + let args = vec!["--yesno", &question.text]; + self.execute(args, &question.title) + .and_then(|output| get_choice(output.status)) + } +} diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 5ae3cef..d5dac1c 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -2,10 +2,12 @@ // SPDX-License-Identifier: MIT mod dialog; +mod kdialog; mod stdio; mod zenity; pub use crate::backends::dialog::Dialog; +pub use crate::backends::kdialog::KDialog; pub use crate::backends::stdio::Stdio; pub use crate::backends::zenity::Zenity; @@ -51,6 +53,7 @@ pub(crate) fn is_available(name: &str) -> bool { pub(crate) fn from_str(s: &str) -> Option<Box<dyn Backend>> { match s.to_lowercase().as_ref() { "dialog" => Some(Box::new(Dialog::new())), + "kdialog" => Some(Box::new(KDialog::new())), "stdio" => Some(Box::new(Stdio::new())), "zenity" => Some(Box::new(Zenity::new())), _ => None, @@ -15,6 +15,8 @@ //! These dialog boxes can be displayed using various backends: //! - [`Dialog`][]: uses `dialog` to display ncurses-based dialog boxes (requires the external //! `dialog` tool) +//! - [`KDialog`][]: uses `kdialog` to display Qt-based dialog boxes (requires the external +//! `kdialog` tool) //! - [`Stdio`][]: prints messages to the standard output and reads user input form standard input //! (intended as a fallback backend) //! - [`Zenity`][]: uses `zenity` to display GTK-based dialog boxes (requires the external `zenity` @@ -72,6 +74,7 @@ //! [`Message`]: struct.Message.html //! [`Password`]: struct.Password.html //! [`Question`]: struct.Question.html +//! [`KDialog`]: backends/struct.KDialog.html //! [`Stdio`]: backends/struct.Stdio.html //! [`Zenity`]: backends/struct.Zenity.html //! [`default_backend`]: fn.default_backend.html @@ -345,13 +348,16 @@ impl DialogBox for Question { /// - If the `DIALOG` environment variable is set to a valid backend name, this backend is used. /// A valid backend name is the name of a struct in the `backends` module implementing the /// `Backend` trait in any case. -/// - If the `DISPLAY` environment variable is set, the first available backend from this list is -/// used: -/// - [`Zenity`][] +/// - If the `DISPLAY` environment variable is set, the following resolution algorithm is used: +/// - If the `XDG_CURRENT_DESKTOP` environment variable is set to `KDE`, [`KDialog`][] is used. +/// - Otherwise, the first available backend from this list is used: +/// - [`Zenity`][] +/// - [`KDialog`][] /// - If the [`Dialog`][] backend is available, it is used. /// - Otherwise, a [`Stdio`][] instance is returned. /// /// [`Dialog`]: backends/struct.Dialog.html +/// [`KDialog`]: backends/struct.KDialog.html /// [`Stdio`]: backends/struct.Stdio.html /// [`Zenity`]: backends/struct.Zenity.html pub fn default_backend() -> Box<dyn backends::Backend> { @@ -363,9 +369,21 @@ pub fn default_backend() -> Box<dyn backends::Backend> { if let Ok(display) = env::var("DISPLAY") { if !display.is_empty() { + // Prefer KDialog if the user is logged into a KDE session + let kdialog_available = backends::KDialog::is_available(); + if let Ok(desktop) = env::var("XDG_CURRENT_DESKTOP") { + if kdialog_available && desktop == "KDE" { + return Box::new(backends::KDialog::new()); + } + } + if backends::Zenity::is_available() { return Box::new(backends::Zenity::new()); } + + if kdialog_available { + return Box::new(backends::KDialog::new()); + } } } |