diff options
Diffstat (limited to 'src/backends')
| -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 | 
5 files changed, 237 insertions, 52 deletions
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) +    }  }  | 
