diff options
author | Robin Krahl <robin.krahl@ireas.org> | 2019-12-10 20:09:59 +0100 |
---|---|---|
committer | Robin Krahl <robin.krahl@ireas.org> | 2019-12-10 20:09:59 +0100 |
commit | aa8148b958daac4b35173a1d28806b2fdc8609bd (patch) | |
tree | d3c438f91a911f8e4bd0fb96dc833edaac4f21e6 | |
parent | 10462c2c9f4ae2ffd3c32dd4628e0052067c15fa (diff) | |
parent | 202c49a327b7a3dc3bfa199da2ac7484ee105f65 (diff) | |
download | dialog-rs-master.tar.gz dialog-rs-master.tar.bz2 |
-rw-r--r-- | .builds/archlinux.yml | 14 | ||||
-rw-r--r-- | .builds/debian.yml | 14 | ||||
-rw-r--r-- | .builds/lint.yml | 27 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | CHANGELOG.md | 11 | ||||
-rw-r--r-- | Cargo.toml | 6 | ||||
-rw-r--r-- | LICENSES/CC0-1.0.txt | 119 | ||||
-rw-r--r-- | LICENSES/MIT.txt (renamed from LICENSE) | 2 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | examples/backend-kdialog.rs | 18 | ||||
-rw-r--r-- | examples/file_selection.rs | 21 | ||||
-rw-r--r-- | src/backends/dialog.rs | 47 | ||||
-rw-r--r-- | src/backends/kdialog.rs | 152 | ||||
-rw-r--r-- | src/backends/mod.rs | 10 | ||||
-rw-r--r-- | src/backends/stdio.rs | 21 | ||||
-rw-r--r-- | src/backends/zenity.rs | 59 | ||||
-rw-r--r-- | src/lib.rs | 131 |
17 files changed, 608 insertions, 60 deletions
diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml new file mode 100644 index 0000000..52e8156 --- /dev/null +++ b/.builds/archlinux.yml @@ -0,0 +1,14 @@ +# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org> +# SPDX-License-Identifier: MIT +image: archlinux +packages: + - rust +sources: + - https://git.ireas.org/dialog-rs +tasks: + - build: | + cd dialog-rs + cargo build --release + - test: | + cd dialog-rs + cargo test diff --git a/.builds/debian.yml b/.builds/debian.yml new file mode 100644 index 0000000..9140c92 --- /dev/null +++ b/.builds/debian.yml @@ -0,0 +1,14 @@ +# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org> +# SPDX-License-Identifier: MIT +image: debian/stable +packages: + - cargo +sources: + - https://git.ireas.org/dialog-rs +tasks: + - build: | + cd dialog-rs + cargo build --release + - test: | + cd dialog-rs + cargo test diff --git a/.builds/lint.yml b/.builds/lint.yml new file mode 100644 index 0000000..6b06f91 --- /dev/null +++ b/.builds/lint.yml @@ -0,0 +1,27 @@ +# Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org> +# SPDX-License-Identifier: MIT +image: archlinux +packages: + - rustup + - python + - python-pip + - python-pygit2 +sources: + - https://git.ireas.org/dialog-rs +tasks: + - setup: | + pip install --user fsfe-reuse + rustup update stable + rustup self upgrade-data + rustup default stable + rustup component add rustfmt + rustup component add clippy + - format: | + cd dialog-rs + cargo fmt -- --check + - reuse: | + cd dialog-rs + ~/.local/bin/reuse lint + - clippy: | + cd dialog-rs + cargo clippy -- -D warnings @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2019 Robin Krahl <robin.krahl@ireas.org> +# SPDX-License-Identifier: CC0-1.0 + /target **/*.rs.bk Cargo.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index edf65a1..afaabb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +<!--- +SPDX-FileCopyrightText: 2019 Robin Krahl <robin.krahl@ireas.org> +SPDX-License-Identifier: CC0-1.0 +--> + +# v0.3.0 (2019-12-10) +- Add the `KDialog` backend (contributed by Stephan Sokolow). +- Add the `FileSelection` dialog (contributed by Reyk Floeter). +- 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 Cortesi for the bug report): @@ -1,6 +1,9 @@ +# SPDX-FileCopyrightText: 2019 Robin Krahl <robin.krahl@ireas.org> +# SPDX-License-Identifier: MIT + [package] name = "dialog" -version = "0.2.1" +version = "0.3.0" authors = ["Robin Krahl <robin.krahl@ireas.org>"] edition = "2018" repository = "https://git.ireas.org/dialog-rs/" @@ -13,3 +16,4 @@ license = "MIT" [dependencies] rpassword = "2" +dirs = "2.0" diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..a343ccd --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,119 @@ +Creative Commons Legal Code + +CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES +NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE +AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION +ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE +OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS +LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION +OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive +Copyright and Related Rights (defined below) upon the creator and subsequent +owner(s) (each and all, an "owner") of an original work of authorship and/or +a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later claims +of infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further production +of creative, cultural and scientific works, or to gain reputation or greater +distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with +a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or +her Copyright and Related Rights in the Work and the meaning and intended +legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected +by copyright and related or neighboring rights ("Copyright and Related Rights"). +Copyright and Related Rights include, but are not limited to, the following: + +i. the right to reproduce, adapt, distribute, perform, display, communicate, +and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + +iii. publicity and privacy rights pertaining to a person's image or likeness +depicted in a Work; + +iv. rights protecting against unfair competition in regards to a Work, subject +to the limitations in paragraph 4(a), below; + +v. rights protecting the extraction, dissemination, use and reuse of data +in a Work; + +vi. database rights (such as those arising under Directive 96/9/EC of the +European Parliament and of the Council of 11 March 1996 on the legal protection +of databases, and under any national implementation thereof, including any +amended or successor version of such directive); and + +vii. other similar, equivalent or corresponding rights throughout the world +based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time extensions), +(iii) in any current or future medium and for any number of copies, and (iv) +for any purpose whatsoever, including without limitation commercial, advertising +or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the +benefit of each member of the public at large and to the detriment of Affirmer's +heirs and successors, fully intending that such Waiver shall not be subject +to revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account Affirmer's +express Statement of Purpose. In addition, to the extent the Waiver is so +judged Affirmer hereby grants to each affected person a royalty-free, non +transferable, non sublicensable, non exclusive, irrevocable and unconditional +license to exercise Affirmer's Copyright and Related Rights in the Work (i) +in all territories worldwide, (ii) for the maximum duration provided by applicable +law or treaty (including future time extensions), (iii) in any current or +future medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional purposes +(the "License"). The License shall be deemed effective as of the date CC0 +was applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder of +the License, and in such case Affirmer hereby affirms that he or she will +not (i) exercise any of his or her remaining Copyright and Related Rights +in the Work or (ii) assert any associated claims and causes of action with +respect to the Work, in either case contrary to Affirmer's express Statement +of Purpose. + + 4. Limitations and Disclaimers. + +a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, +licensed or otherwise affected by this document. + +b. Affirmer offers the Work as-is and makes no representations or warranties +of any kind concerning the Work, express, implied, statutory or otherwise, +including without limitation warranties of title, merchantability, fitness +for a particular purpose, non infringement, or the absence of latent or other +defects, accuracy, or the present or absence of errors, whether or not discoverable, +all to the greatest extent permissible under applicable law. + +c. Affirmer disclaims responsibility for clearing rights of other persons +that may apply to the Work or any use thereof, including without limitation +any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims +responsibility for obtaining any necessary consents, permissions or other +rights required for any use of the Work. + +d. Affirmer understands and acknowledges that Creative Commons is not a party +to this document and has no duty or obligation with respect to this CC0 or +use of the Work. diff --git a/LICENSE b/LICENSES/MIT.txt index 1a3601d..741cbc3 100644 --- a/LICENSE +++ b/LICENSES/MIT.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 Robin Krahl <robin.krahl@ireas.org> +Copyright (c) 2019 Robin Krahl <robin.krahl@ireas.org> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -1,3 +1,8 @@ +<!--- +SPDX-FileCopyrightText: 2019 Robin Krahl <robin.krahl@ireas.org> +SPDX-License-Identifier: MIT +--> + # dialog-rs A Rust library for displaying dialog boxes using various backends. @@ -5,8 +10,9 @@ A Rust library for displaying dialog boxes using various backends. [Documentation][] Currently `dialog-rs` supports input, message, password and question dialogs. -It can use the `dialog` or `zenity` tools to display the dialog boxes. If none -of these tools is available, the dialogs are printed to the standard output. +It can use the `dialog`, `kdialog`, or `zenity` tools to display the dialog +boxes. If none of these tools is available, the dialogs are printed to the +standard output. ## Example @@ -34,6 +40,9 @@ mail to [dialog-rs-dev@ireas.org][]. This project is licensed under the [MIT License][]. +`dialog-rs` complies with [version 3.0 of the REUSE specification][reuse]. + [Documentation]: https://docs.rs/dialog [dialog-rs-dev@ireas.org]: mailto:dialog-rs-dev@ireas.org [MIT license]: https://opensource.org/licenses/MIT +[reuse]: https://reuse.software/practices/3.0/ diff --git a/examples/backend-kdialog.rs b/examples/backend-kdialog.rs new file mode 100644 index 0000000..5c3fb46 --- /dev/null +++ b/examples/backend-kdialog.rs @@ -0,0 +1,18 @@ +// Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org> +// SPDX-License-Identifier: MIT + +use dialog::backends; +use dialog::DialogBox; + +fn main() -> dialog::Result<()> { + let mut backend = backends::KDialog::new(); + + dialog::Message::new("This is a message.") + .title("And this is a title:") + .show_with(&backend)?; + + backend.set_icon("error"); + dialog::Message::new("This is an error message.") + .title("Error") + .show_with(&backend) +} diff --git a/examples/file_selection.rs b/examples/file_selection.rs new file mode 100644 index 0000000..5d2d860 --- /dev/null +++ b/examples/file_selection.rs @@ -0,0 +1,21 @@ +// Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org> +// SPDX-License-Identifier: MIT + +use dialog::DialogBox; + +fn main() -> dialog::Result<()> { + let choice = dialog::FileSelection::new("Please select a file") + .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/dialog.rs b/src/backends/dialog.rs index e681caf..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. /// @@ -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<Dialog> 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(()) @@ -111,19 +117,17 @@ fn get_choice(status: process::ExitStatus) -> Result<Choice> { fn get_stderr(output: process::Output) -> Result<Option<String>> { 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))) } } @@ -156,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<Option<String>> { + 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/kdialog.rs b/src/backends/kdialog.rs new file mode 100644 index 0000000..21928f8 --- /dev/null +++ b/src/backends/kdialog.rs @@ -0,0 +1,152 @@ +// 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, FileSelection, FileSelectionMode, 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, Default)] +pub struct KDialog { + icon: Option<String>, + // TODO: --dontagain +} + +impl KDialog { + /// Creates a new `KDialog` instance without configuration. + pub fn new() -> KDialog { + Default::default() + } + + /// 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(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))) + } +} + +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)) + } + + fn show_file_selection(&self, file_selection: &FileSelection) -> Result<Option<String>> { + let dir = file_selection.path_to_string().ok_or("path not valid")?; + 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/mod.rs b/src/backends/mod.rs index 5ae3cef..09013b1 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; @@ -21,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. @@ -35,11 +37,14 @@ pub trait Backend { /// Shows the given question dialog and returns the choice. fn show_question(&self, question: &super::Question) -> Result<super::Choice>; + + /// Shows the given file selection dialog and returns the file name. + fn show_file_selection(&self, file_selection: &super::FileSelection) -> Result<Option<String>>; } 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; } @@ -51,6 +56,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, diff --git a/src/backends/stdio.rs b/src/backends/stdio.rs index 6838ef1..627714a 100644 --- a/src/backends/stdio.rs +++ b/src/backends/stdio.rs @@ -3,19 +3,19 @@ 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. /// /// 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() } } @@ -35,7 +35,7 @@ fn print_title(title: &Option<String>) { fn read_input() -> Result<String> { 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 { @@ -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<Option<String>> { + 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 85206b3..ac0f98f 100644 --- a/src/backends/zenity.rs +++ b/src/backends/zenity.rs @@ -3,12 +3,14 @@ use std::process; -use crate::{Choice, Error, Input, Message, Password, Question, Result}; +use crate::{ + Choice, Error, FileSelection, FileSelectionMode, 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<String>, width: Option<String>, @@ -19,17 +21,12 @@ 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. /// - /// 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<String>) { self.icon = Some(icon.into()); @@ -101,15 +98,13 @@ impl AsRef<Zenity> 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 +125,16 @@ 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 { - 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))) } } @@ -172,4 +165,14 @@ 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<Option<String>> { + let dir = file_selection.path_to_string().ok_or("path not valid")?; + 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) + } } @@ -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 @@ -15,6 +16,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` @@ -68,10 +71,12 @@ //! ``` //! //! [`Dialog`]: backends/struct.Dialog.html +//! [`FileSelection`]: struct.FileSelection.html //! [`Input`]: struct.Input.html //! [`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 @@ -89,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}; @@ -339,19 +348,121 @@ 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 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 +/// 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); +/// ``` +/// +/// [`FileSelectionMode`]: enum.FileSelectionMode.html +pub struct FileSelection { + text: String, + title: Option<String>, + path: Option<PathBuf>, + mode: FileSelectionMode, +} + +impl FileSelection { + /// Creates a new file chooser with the given path. + pub fn new(text: impl Into<String>) -> FileSelection { + FileSelection { + text: text.into(), + title: None, + path: dirs::home_dir(), + mode: FileSelectionMode::Open, + } + } + + /// 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<String>) -> &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<Path>) -> &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<String> { + 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, + } + } + + /// 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 { + type Output = Option<String>; + + fn show_with<B>(&self, backend: impl AsRef<B>) -> Result<Self::Output> + 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: /// - 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 +474,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()); + } } } |