aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Krahl <robin.krahl@ireas.org>2019-12-10 20:09:59 +0100
committerRobin Krahl <robin.krahl@ireas.org>2019-12-10 20:09:59 +0100
commitaa8148b958daac4b35173a1d28806b2fdc8609bd (patch)
treed3c438f91a911f8e4bd0fb96dc833edaac4f21e6
parent10462c2c9f4ae2ffd3c32dd4628e0052067c15fa (diff)
parent202c49a327b7a3dc3bfa199da2ac7484ee105f65 (diff)
downloaddialog-rs-aa8148b958daac4b35173a1d28806b2fdc8609bd.tar.gz
dialog-rs-aa8148b958daac4b35173a1d28806b2fdc8609bd.tar.bz2
Merge branch 'release-v0.3.0'HEADmaster
-rw-r--r--.builds/archlinux.yml14
-rw-r--r--.builds/debian.yml14
-rw-r--r--.builds/lint.yml27
-rw-r--r--.gitignore3
-rw-r--r--CHANGELOG.md11
-rw-r--r--Cargo.toml6
-rw-r--r--LICENSES/CC0-1.0.txt119
-rw-r--r--LICENSES/MIT.txt (renamed from LICENSE)2
-rw-r--r--README.md13
-rw-r--r--examples/backend-kdialog.rs18
-rw-r--r--examples/file_selection.rs21
-rw-r--r--src/backends/dialog.rs47
-rw-r--r--src/backends/kdialog.rs152
-rw-r--r--src/backends/mod.rs10
-rw-r--r--src/backends/stdio.rs21
-rw-r--r--src/backends/zenity.rs59
-rw-r--r--src/lib.rs131
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
diff --git a/.gitignore b/.gitignore
index 2fcd2ae..6f7571a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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):
diff --git a/Cargo.toml b/Cargo.toml
index 2f11081..7fa888f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
diff --git a/README.md b/README.md
index d2439b0..1c6f6d0 100644
--- a/README.md
+++ b/README.md
@@ -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)
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index f4ac763..aa19105 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
@@ -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());
+ }
}
}