diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | examples/file_selection.rs | 21 | ||||
| -rw-r--r-- | src/backends/dialog.rs | 9 | ||||
| -rw-r--r-- | src/backends/kdialog.rs | 15 | ||||
| -rw-r--r-- | src/backends/mod.rs | 3 | ||||
| -rw-r--r-- | src/backends/stdio.rs | 15 | ||||
| -rw-r--r-- | src/backends/zenity.rs | 14 | ||||
| -rw-r--r-- | src/lib.rs | 107 | 
9 files changed, 181 insertions, 5 deletions
| diff --git a/CHANGELOG.md b/CHANGELOG.md index 939ae76..99a219f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ SPDX-License-Identifier: CC0-1.0  # Unreleased  - 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. @@ -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..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 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<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 index c2ddcd0..21928f8 100644 --- a/src/backends/kdialog.rs +++ b/src/backends/kdialog.rs @@ -4,7 +4,9 @@  use std::process; -use crate::{Choice, Error, Input, Message, Password, Question, Result}; +use crate::{ +    Choice, Error, FileSelection, FileSelectionMode, Input, Message, Password, Question, Result, +};  /// Subprocess exit codes  /// @@ -136,4 +138,15 @@ 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<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 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<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 { 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<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 0deca0a..ac0f98f 100644 --- a/src/backends/zenity.rs +++ b/src/backends/zenity.rs @@ -3,7 +3,9 @@  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.  /// @@ -163,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 @@ -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,105 @@ 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: | 
