From a23de9787bccde4b6244312f04cb7cfc3b528db6 Mon Sep 17 00:00:00 2001 From: Stephan Sokolow Date: Thu, 24 Oct 2019 20:36:42 -0400 Subject: Add a backend based on KDE's `kdialog` --- src/backends/kdialog.rs | 143 ++++++++++++++++++++++++++++++++++++++++++++++++ src/backends/mod.rs | 3 + src/lib.rs | 24 +++++++- 3 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 src/backends/kdialog.rs (limited to 'src') 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 +// Copyright (C) 2019 Stephan Sokolow +// 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, + // 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) { + self.icon = Some(icon.into()); + } + + pub(crate) fn is_available() -> bool { + super::is_available("kdialog") + } + + fn execute(&self, args: Vec<&str>, title: &Option) -> Result { + 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 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 { + 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> { + 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> { + 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> { + let args = vec!["--password", &password.text]; + self.execute(args, &password.title).and_then(get_stdout) + } + + fn show_question(&self, question: &Question) -> Result { + 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> { 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, diff --git a/src/lib.rs b/src/lib.rs index f4ac763..61be22b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 { @@ -363,9 +369,21 @@ pub fn default_backend() -> Box { 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()); + } } } -- cgit v1.2.3 From 9a2e1124bc97a003ad8916a17653ff2d1fa4250d Mon Sep 17 00:00:00 2001 From: Stephan Sokolow Date: Thu, 24 Oct 2019 20:37:03 -0400 Subject: Fix broken link in `backends` documentation (Revealed by cargo-deadlinks) --- src/backends/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/backends/mod.rs b/src/backends/mod.rs index d5dac1c..f69322f 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -23,7 +23,7 @@ use crate::Result; /// backend and create an instance manually. To use a backend, pass it to the [`show_with`][] /// method of a dialog box. /// -/// [`default_backend`]: ../function.default_backend.html +/// [`default_backend`]: ../fn.default_backend.html /// [`show_with`]: ../trait.DialogBox.html#method.show_with pub trait Backend { /// Shows the given input dialog and returns the input. -- cgit v1.2.3 From 9273f3f45f7418e87b6e53baef78612ef0d6c5e7 Mon Sep 17 00:00:00 2001 From: Stephan Sokolow Date: Thu, 24 Oct 2019 20:44:39 -0400 Subject: Fix clippy complaints that don't change the semantics (Clippy also complains about `new()` without `impl Default`) --- src/backends/dialog.rs | 22 ++++++++++------------ src/backends/kdialog.rs | 32 ++++++++++++++------------------ src/backends/mod.rs | 2 +- src/backends/stdio.rs | 2 +- src/backends/zenity.rs | 34 +++++++++++++++------------------- 5 files changed, 41 insertions(+), 51 deletions(-) (limited to 'src') diff --git a/src/backends/dialog.rs b/src/backends/dialog.rs index e681caf..85b0294 100644 --- a/src/backends/dialog.rs +++ b/src/backends/dialog.rs @@ -111,19 +111,17 @@ fn get_choice(status: process::ExitStatus) -> Result { fn get_stderr(output: process::Output) -> Result> { if output.status.success() { String::from_utf8(output.stderr) - .map(|s| Some(s)) - .map_err(|err| Error::from(err)) - } else { - if let Some(code) = output.status.code() { - match code { - 0 => Ok(None), - 1 => Ok(None), - 255 => Ok(None), - _ => Err(Error::from(("dialog", output.status))), - } - } else { - Err(Error::from(("dialog", output.status))) + .map(Some) + .map_err(Error::from) + } else if let Some(code) = output.status.code() { + match code { + 0 => Ok(None), + 1 => Ok(None), + 255 => Ok(None), + _ => Err(Error::from(("dialog", output.status))), } + } else { + Err(Error::from(("dialog", output.status))) } } diff --git a/src/backends/kdialog.rs b/src/backends/kdialog.rs index 153e4c5..34070c4 100644 --- a/src/backends/kdialog.rs +++ b/src/backends/kdialog.rs @@ -72,15 +72,13 @@ impl AsRef for KDialog { 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))) + } else if let Some(code) = status.code() { + match code { + CANCEL => Ok(()), + _ => Err(Error::from(("kdialog", status))), } + } else { + Err(Error::from(("kdialog", status))) } } @@ -100,17 +98,15 @@ fn get_stdout(output: process::Output) -> Result> { 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))) + .map_err(Error::from) + } 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))) } } diff --git a/src/backends/mod.rs b/src/backends/mod.rs index f69322f..1331323 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -41,7 +41,7 @@ pub trait Backend { pub(crate) fn is_available(name: &str) -> bool { if let Ok(path) = env::var("PATH") { - for part in path.split(":") { + for part in path.split(':') { if path::Path::new(part).join(name).exists() { return true; } diff --git a/src/backends/stdio.rs b/src/backends/stdio.rs index 6838ef1..4e4c8ec 100644 --- a/src/backends/stdio.rs +++ b/src/backends/stdio.rs @@ -35,7 +35,7 @@ fn print_title(title: &Option) { fn read_input() -> Result { let mut input = String::new(); io::stdin().read_line(&mut input)?; - Ok(input.trim_end_matches("\n").to_string()) + Ok(input.trim_end_matches('\n').to_string()) } fn parse_choice(input: &str) -> Choice { diff --git a/src/backends/zenity.rs b/src/backends/zenity.rs index 85206b3..12e052b 100644 --- a/src/backends/zenity.rs +++ b/src/backends/zenity.rs @@ -101,15 +101,13 @@ impl AsRef for Zenity { fn require_success(status: process::ExitStatus) -> Result<()> { if status.success() { Ok(()) - } else { - if let Some(code) = status.code() { - match code { - 5 => Ok(()), - _ => Err(Error::from(("zenity", status))), - } - } else { - Err(Error::from(("zenity", status))) + } else if let Some(code) = status.code() { + match code { + 5 => Ok(()), + _ => Err(Error::from(("zenity", status))), } + } else { + Err(Error::from(("zenity", status))) } } @@ -130,18 +128,16 @@ fn get_stdout(output: process::Output) -> Result> { 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 { - 0 => Ok(None), - 1 => Ok(None), - 5 => Ok(None), - _ => Err(Error::from(("zenity", output.status))), - } - } else { - Err(Error::from(("zenity", output.status))) + .map_err(Error::from) + } else if let Some(code) = output.status.code() { + match code { + 0 => Ok(None), + 1 => Ok(None), + 5 => Ok(None), + _ => Err(Error::from(("zenity", output.status))), } + } else { + Err(Error::from(("zenity", output.status))) } } -- cgit v1.2.3 From dfe809ae6c191cbc8478363bc5b0ed2626f91bb0 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 26 Oct 2019 13:07:41 +0200 Subject: Add missing backtick in doc comment --- src/backends/zenity.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/backends/zenity.rs b/src/backends/zenity.rs index 12e052b..d5745eb 100644 --- a/src/backends/zenity.rs +++ b/src/backends/zenity.rs @@ -29,7 +29,7 @@ impl Zenity { /// Sets the icon of the dialog box. /// - /// The icon can either be one of `error`, `info`, `question` or `warning, or the path to an + /// The icon can either be one of `error`, `info`, `question` or `warning`, 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) { self.icon = Some(icon.into()); -- cgit v1.2.3 From 0970381271bd07f020a888b027653ca627e8655c Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sun, 27 Oct 2019 10:55:30 +0000 Subject: Implement Default for all backend structs This fixes the clippy warning new_without_default [0]. [0] https://rust-lang.github.io/rust-clippy/master/index.html#new_without_default --- CHANGELOG.md | 1 + src/backends/dialog.rs | 16 +++++++++++----- src/backends/kdialog.rs | 4 ++-- src/backends/stdio.rs | 4 ++-- src/backends/zenity.rs | 9 ++------- 5 files changed, 18 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6256c..939ae76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ SPDX-License-Identifier: CC0-1.0 # Unreleased - Add the `KDialog` backend (contributed by Stephan Sokolow). - Comply with version 3.0 of the REUSE specification. +- Implement `Default` for all backend structs. # v0.2.1 (2019-06-30) - Fix the input and password dialogs for the `zenity` backend (thanks Silvano diff --git a/src/backends/dialog.rs b/src/backends/dialog.rs index 85b0294..8061d98 100644 --- a/src/backends/dialog.rs +++ b/src/backends/dialog.rs @@ -19,11 +19,7 @@ pub struct Dialog { impl Dialog { /// Creates a new `Dialog` instance without configuration. pub fn new() -> Dialog { - Dialog { - backtitle: None, - height: "0".to_string(), - width: "0".to_string(), - } + Default::default() } /// Sets the backtitle for the dialog boxes. @@ -87,6 +83,16 @@ impl AsRef for Dialog { } } +impl Default for Dialog { + fn default() -> Self { + Dialog { + backtitle: None, + height: "0".to_string(), + width: "0".to_string(), + } + } +} + fn require_success(status: process::ExitStatus) -> Result<()> { if status.success() { Ok(()) diff --git a/src/backends/kdialog.rs b/src/backends/kdialog.rs index 34070c4..c2ddcd0 100644 --- a/src/backends/kdialog.rs +++ b/src/backends/kdialog.rs @@ -20,7 +20,7 @@ const CANCEL: i32 = 1; /// The `kdialog` backend. /// /// This backend uses the external `kdialog` program to display KDE dialog boxes. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct KDialog { icon: Option, // TODO: --dontagain @@ -29,7 +29,7 @@ pub struct KDialog { impl KDialog { /// Creates a new `KDialog` instance without configuration. pub fn new() -> KDialog { - KDialog { icon: None } + Default::default() } /// Sets the icon in the dialog box's titlebar and taskbar button. diff --git a/src/backends/stdio.rs b/src/backends/stdio.rs index 4e4c8ec..9a153df 100644 --- a/src/backends/stdio.rs +++ b/src/backends/stdio.rs @@ -9,13 +9,13 @@ use crate::{Choice, Input, Message, Password, Question, Result}; /// /// This backend is intended as a fallback backend to use if no other backend is available. The /// dialogs are printed to the standard output and user input is read from the standard input. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Stdio {} impl Stdio { /// Creates a new `Stdio` instance. pub fn new() -> Stdio { - Stdio {} + Default::default() } } diff --git a/src/backends/zenity.rs b/src/backends/zenity.rs index d5745eb..0deca0a 100644 --- a/src/backends/zenity.rs +++ b/src/backends/zenity.rs @@ -8,7 +8,7 @@ use crate::{Choice, Error, Input, Message, Password, Question, Result}; /// The `zenity` backend. /// /// This backend uses the external `zenity` program to display GTK+ dialog boxes. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Zenity { icon: Option, width: Option, @@ -19,12 +19,7 @@ pub struct Zenity { impl Zenity { /// Creates a new `Zenity` instance without configuration. pub fn new() -> Zenity { - Zenity { - icon: None, - width: None, - height: None, - timeout: None, - } + Default::default() } /// Sets the icon of the dialog box. -- cgit v1.2.3 From 2f3e2b5474834e3d733edf09b831c5607d451f49 Mon Sep 17 00:00:00 2001 From: Reyk Floeter Date: Tue, 10 Dec 2019 15:24:36 +0000 Subject: Add FileSelection dialog type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds the FileSelection struct representing a file selection dialog. It can be displayed using the backend’s show_file_selection function. Currently, we only support file open dialogs (i. e. choosing an existing file). Support for save dialogs should be added in the future. --- Cargo.toml | 1 + examples/file_selection.rs | 13 ++++++++ src/backends/dialog.rs | 9 ++++- src/backends/mod.rs | 3 ++ src/backends/stdio.rs | 15 ++++++++- src/backends/zenity.rs | 9 ++++- src/lib.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 examples/file_selection.rs (limited to 'src') diff --git a/Cargo.toml b/Cargo.toml index 57bc68f..355b789 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ license = "MIT" [dependencies] rpassword = "2" +dirs = "2.0" diff --git a/examples/file_selection.rs b/examples/file_selection.rs new file mode 100644 index 0000000..5436742 --- /dev/null +++ b/examples/file_selection.rs @@ -0,0 +1,13 @@ +// Copyright (C) 2019 Robin Krahl +// SPDX-License-Identifier: MIT + +use dialog::DialogBox; + +fn main() -> dialog::Result<()> { + let choice = dialog::FileSelection::new("Please select a file") + .title("File Chooser Example") + .path("/etc") + .show()?; + println!("The user chose: {:?}", choice); + Ok(()) +} diff --git a/src/backends/dialog.rs b/src/backends/dialog.rs index 8061d98..668cf57 100644 --- a/src/backends/dialog.rs +++ b/src/backends/dialog.rs @@ -3,7 +3,7 @@ use std::process; -use crate::{Choice, Error, Input, Message, Password, Question, Result}; +use crate::{Choice, Error, FileSelection, Input, Message, Password, Question, Result}; /// The `dialog` backend. /// @@ -160,4 +160,11 @@ impl super::Backend for Dialog { self.execute(args, vec![], &question.title) .and_then(|output| get_choice(output.status)) } + + fn show_file_selection(&self, file_selection: &FileSelection) -> Result> { + let dir = file_selection.path_to_string().ok_or("path not valid")?; + let args = vec!["--fselect", &dir]; + self.execute(args, vec![], &file_selection.title) + .and_then(get_stderr) + } } diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 1331323..09013b1 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -37,6 +37,9 @@ pub trait Backend { /// Shows the given question dialog and returns the choice. fn show_question(&self, question: &super::Question) -> Result; + + /// Shows the given file selection dialog and returns the file name. + fn show_file_selection(&self, file_selection: &super::FileSelection) -> Result>; } pub(crate) fn is_available(name: &str) -> bool { diff --git a/src/backends/stdio.rs b/src/backends/stdio.rs index 9a153df..627714a 100644 --- a/src/backends/stdio.rs +++ b/src/backends/stdio.rs @@ -3,7 +3,7 @@ use std::io::{self, Write}; -use crate::{Choice, Input, Message, Password, Question, Result}; +use crate::{Choice, FileSelection, Input, Message, Password, Question, Result}; /// The fallback backend using standard input and output. /// @@ -86,4 +86,17 @@ impl super::Backend for Stdio { io::stdout().flush()?; Ok(parse_choice(&read_input()?)) } + + fn show_file_selection(&self, file_selection: &FileSelection) -> Result> { + let dir = file_selection.path_to_string().ok_or("path not valid")?; + print_title(&file_selection.title); + print!("{} [{}]: ", file_selection.text, dir); + io::stdout().flush()?; + let result = read_input()?; + if result.starts_with('/') { + Ok(Some(result)) + } else { + Ok(Some(dir + &result)) + } + } } diff --git a/src/backends/zenity.rs b/src/backends/zenity.rs index 0deca0a..4ee7785 100644 --- a/src/backends/zenity.rs +++ b/src/backends/zenity.rs @@ -3,7 +3,7 @@ use std::process; -use crate::{Choice, Error, Input, Message, Password, Question, Result}; +use crate::{Choice, Error, FileSelection, Input, Message, Password, Question, Result}; /// The `zenity` backend. /// @@ -163,4 +163,11 @@ impl super::Backend for Zenity { self.execute(args, &question.title) .and_then(|output| get_choice(output.status)) } + + fn show_file_selection(&self, file_selection: &FileSelection) -> Result> { + let dir = file_selection.path_to_string().ok_or("path not valid")?; + let args = vec!["--file-selection", "--filename", &dir]; + self.execute(args, &file_selection.title) + .and_then(get_stdout) + } } diff --git a/src/lib.rs b/src/lib.rs index 61be22b..da2a259 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ //! //! The `dialog` crate can be used to display different types of dialog boxes. The supported types //! are: +//! - [`FileSelection`][]: a file chooser dialog box //! - [`Input`][]: a text input dialog //! - [`Message`][]: a simple message box //! - [`Password`][]: a password input dialog @@ -70,6 +71,7 @@ //! ``` //! //! [`Dialog`]: backends/struct.Dialog.html +//! [`FileSelection`]: struct.FileSelection.html //! [`Input`]: struct.Input.html //! [`Message`]: struct.Message.html //! [`Password`]: struct.Password.html @@ -92,7 +94,11 @@ mod error; /// [`Backend`]: trait.Backend.html pub mod backends; -use std::env; +use dirs; +use std::{ + env, + path::{Path, PathBuf}, +}; pub use crate::error::{Error, Result}; @@ -342,6 +348,80 @@ impl DialogBox for Question { } } +/// A file chooser dialog box. +/// +/// This dialog box opens a file chooser with an optional title in the specified path. If the path +/// is not specified, it defaults to the user’s home directory. +/// +/// # Example +/// +/// ```no_run +/// use dialog::DialogBox; +/// +/// let choice = dialog::FileSelection::new("Please select a file") +/// .title("File Selection") +/// .path("/home/user/Downloads") +/// .show() +/// .expect("Could not display dialog box"); +/// println!("The user chose: {:?}", choice); +/// ``` +pub struct FileSelection { + text: String, + title: Option, + path: Option, +} + +impl FileSelection { + /// Creates a new file chooser with the given path. + pub fn new(text: impl Into) -> FileSelection { + FileSelection { + text: text.into(), + title: None, + path: dirs::home_dir(), + } + } + + /// Sets the title of this file chooser dialog box. + /// + /// This method returns a reference to `self` to enable chaining. + pub fn title(&mut self, title: impl Into) -> &mut FileSelection { + self.title = Some(title.into()); + self + } + + /// Sets the path of this file chooser dialog box. + /// + /// This method returns a reference to `self` to enable chaining. + pub fn path(&mut self, path: impl AsRef) -> &mut FileSelection { + self.path = Some(path.as_ref().to_path_buf()); + self + } + + /// Gets the path of this file chooser dialog box. + /// + /// This method returns the validated directory as a `String`. + pub fn path_to_string(&self) -> Option { + match self.path { + Some(ref path) if path.is_dir() => { + // The backends expect a trailing / after the directory + path.to_str().map(|s| s.to_string() + "/") + } + _ => None, + } + } +} + +impl DialogBox for FileSelection { + type Output = Option; + + fn show_with(&self, backend: impl AsRef) -> Result + where + B: backends::Backend + ?Sized, + { + backend.as_ref().show_file_selection(self) + } +} + /// Creates a new instance of the default backend. /// /// The following steps are performed to determine the default backend: -- cgit v1.2.3 From 1b50ae033b646db0efc8ea0917685a3a0c8bfc94 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 10 Dec 2019 18:10:30 +0000 Subject: Implement show_file_selection for kdialog backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds an implementation of Backend’s show_file_selection function to the KDialog backend, using KDialog’s --getopenfilename option. --- src/backends/kdialog.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/backends/kdialog.rs b/src/backends/kdialog.rs index c2ddcd0..e556ddf 100644 --- a/src/backends/kdialog.rs +++ b/src/backends/kdialog.rs @@ -4,7 +4,7 @@ use std::process; -use crate::{Choice, Error, Input, Message, Password, Question, Result}; +use crate::{Choice, Error, FileSelection, Input, Message, Password, Question, Result}; /// Subprocess exit codes /// @@ -136,4 +136,11 @@ impl super::Backend for KDialog { self.execute(args, &question.title) .and_then(|output| get_choice(output.status)) } + + fn show_file_selection(&self, file_selection: &FileSelection) -> Result> { + let dir = file_selection.path_to_string().ok_or("path not valid")?; + let args = vec!["--getopenfilename", &dir]; + self.execute(args, &file_selection.title) + .and_then(get_stdout) + } } -- cgit v1.2.3 From 89afcb4844dd484f0c9cdfef7e5ff8d751647c43 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 10 Dec 2019 18:46:44 +0000 Subject: Add Open/Save mode to the file selection dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds the option to set a FileSelectionMode, either Open or Save. Not all backends might support this – currently, only zenity and kdialog do. Per default, the Open mode is used (as before). --- examples/file_selection.rs | 10 +++++++++- src/backends/kdialog.rs | 10 ++++++++-- src/backends/zenity.rs | 9 +++++++-- src/lib.rs | 27 ++++++++++++++++++++++++++- 4 files changed, 50 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/examples/file_selection.rs b/examples/file_selection.rs index 5436742..5d2d860 100644 --- a/examples/file_selection.rs +++ b/examples/file_selection.rs @@ -5,9 +5,17 @@ use dialog::DialogBox; fn main() -> dialog::Result<()> { let choice = dialog::FileSelection::new("Please select a file") - .title("File Chooser Example") + .title("File Chooser Example (Open)") .path("/etc") .show()?; println!("The user chose: {:?}", choice); + + let choice = dialog::FileSelection::new("Please select a file") + .title("File Chooser Example (Save)") + .mode(dialog::FileSelectionMode::Save) + .path("/etc") + .show()?; + println!("The user chose: {:?}", choice); + Ok(()) } diff --git a/src/backends/kdialog.rs b/src/backends/kdialog.rs index e556ddf..21928f8 100644 --- a/src/backends/kdialog.rs +++ b/src/backends/kdialog.rs @@ -4,7 +4,9 @@ use std::process; -use crate::{Choice, Error, FileSelection, Input, Message, Password, Question, Result}; +use crate::{ + Choice, Error, FileSelection, FileSelectionMode, Input, Message, Password, Question, Result, +}; /// Subprocess exit codes /// @@ -139,7 +141,11 @@ impl super::Backend for KDialog { fn show_file_selection(&self, file_selection: &FileSelection) -> Result> { let dir = file_selection.path_to_string().ok_or("path not valid")?; - let args = vec!["--getopenfilename", &dir]; + let option = match file_selection.mode { + FileSelectionMode::Open => "--getopenfilename", + FileSelectionMode::Save => "--getsavefilename", + }; + let args = vec![option, &dir]; self.execute(args, &file_selection.title) .and_then(get_stdout) } diff --git a/src/backends/zenity.rs b/src/backends/zenity.rs index 4ee7785..ac0f98f 100644 --- a/src/backends/zenity.rs +++ b/src/backends/zenity.rs @@ -3,7 +3,9 @@ use std::process; -use crate::{Choice, Error, FileSelection, Input, Message, Password, Question, Result}; +use crate::{ + Choice, Error, FileSelection, FileSelectionMode, Input, Message, Password, Question, Result, +}; /// The `zenity` backend. /// @@ -166,7 +168,10 @@ impl super::Backend for Zenity { fn show_file_selection(&self, file_selection: &FileSelection) -> Result> { let dir = file_selection.path_to_string().ok_or("path not valid")?; - let args = vec!["--file-selection", "--filename", &dir]; + let mut args = vec!["--file-selection", "--filename", &dir]; + if file_selection.mode == FileSelectionMode::Save { + args.push("--save"); + } self.execute(args, &file_selection.title) .and_then(get_stdout) } diff --git a/src/lib.rs b/src/lib.rs index da2a259..aa19105 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -348,11 +348,24 @@ impl DialogBox for Question { } } +/// The type of a file selection dialog. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FileSelectionMode { + /// An Open File dialog, meaning that the user can only select an existing file. + Open, + /// A Save File dialog, meaning that the user is allowed to select a non-existing file. + Save, +} + /// A file chooser dialog box. /// -/// This dialog box opens a file chooser with an optional title in the specified path. If the path +/// This dialog box opens a file choser with an optional title in the specified path. If the path /// is not specified, it defaults to the user’s home directory. /// +/// The backends might support multiple operation modes, for example open or save dialogs. You can +/// select a mode using the [`FileSelectionMode`][] enum, though the backend might ignore the mode +/// and just display a simple file dialog. Per default, the mode is set to `Open`. +/// /// # Example /// /// ```no_run @@ -365,10 +378,13 @@ impl DialogBox for Question { /// .expect("Could not display dialog box"); /// println!("The user chose: {:?}", choice); /// ``` +/// +/// [`FileSelectionMode`]: enum.FileSelectionMode.html pub struct FileSelection { text: String, title: Option, path: Option, + mode: FileSelectionMode, } impl FileSelection { @@ -378,6 +394,7 @@ impl FileSelection { text: text.into(), title: None, path: dirs::home_dir(), + mode: FileSelectionMode::Open, } } @@ -409,6 +426,14 @@ impl FileSelection { _ => None, } } + + /// Sets the operation mode of the file chooser. + /// + /// This method returns a reference to `self` to enable chaining. + pub fn mode(&mut self, mode: FileSelectionMode) -> &mut FileSelection { + self.mode = mode; + self + } } impl DialogBox for FileSelection { -- cgit v1.2.3