Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zenity implementation #106

Merged
merged 1 commit into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ windows = { version = "0.44", features = [
] }

[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
# Make sure that this is in sync with zbus, to avoid duplicated deps
async-io = "1.3"
futures-util = { version = "0.3", default-features = false, features = ["io"] }

# XDG Desktop Portal
ashpd = { version = "0.3", optional = true }
urlencoding = { version = "2.1.0", optional = true }
Expand Down
12 changes: 12 additions & 0 deletions src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;

#[cfg(all(
any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
),
not(feature = "gtk3")
))]
mod linux;

#[cfg(all(
any(
target_os = "linux",
Expand Down
26 changes: 26 additions & 0 deletions src/backend/linux/child_stdout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use std::{
io,
pin::Pin,
task::{Context, Poll},
};

use async_io::Async;
use futures_util::AsyncRead;

pub struct ChildStdout(Async<std::process::ChildStdout>);

impl ChildStdout {
pub fn new(stdout: std::process::ChildStdout) -> io::Result<Self> {
Async::new(stdout).map(Self)
}
}

impl AsyncRead for ChildStdout {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.0).poll_read(cx, buf)
}
}
2 changes: 2 additions & 0 deletions src/backend/linux/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod child_stdout;
pub(crate) mod zenity;
273 changes: 273 additions & 0 deletions src/backend/linux/zenity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
use futures_util::AsyncReadExt;
use std::{
error::Error,
fmt::Display,
path::PathBuf,
process::{Command, Stdio},
time::Duration,
};

use super::child_stdout::ChildStdout;
use crate::{
file_dialog::Filter,
message_dialog::{MessageButtons, MessageLevel},
FileDialog,
};

#[derive(Debug)]
pub enum ZenityError {
Io(std::io::Error),
StdOutNotFound,
}

impl Error for ZenityError {}

impl Display for ZenityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ZenityError::Io(io) => write!(f, "{io}"),
ZenityError::StdOutNotFound => write!(f, "Stdout not found"),
}
}
}

impl From<std::io::Error> for ZenityError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}

pub type ZenityResult<T> = Result<T, ZenityError>;

fn command() -> Command {
Command::new("zenity")
}

fn add_filters(command: &mut Command, filters: &[Filter]) {
for f in filters.iter() {
command.arg("--file-filter");

let extensions: Vec<_> = f
.extensions
.iter()
.map(|ext| format!("*.{}", ext))
.collect();

command.arg(format!("{} | {}", f.name, extensions.join(" ")));
}
}

fn add_filename(command: &mut Command, file_name: &Option<String>) {
if let Some(name) = file_name.as_ref() {
command.arg("--filename");
command.arg(name);
}
}

async fn run(mut command: Command) -> ZenityResult<Option<String>> {
let mut process = command.stdout(Stdio::piped()).spawn()?;

let stdout = process.stdout.take().ok_or(ZenityError::StdOutNotFound)?;
let mut stdout = ChildStdout::new(stdout)?;

let mut buffer = String::new();
stdout.read_to_string(&mut buffer).await?;

let status = loop {
if let Some(status) = process.try_wait()? {
break status;
}

async_io::Timer::after(Duration::from_millis(1)).await;
};

Ok(if status.success() { Some(buffer) } else { None })
}

#[allow(unused)]
pub async fn pick_file(dialog: &FileDialog) -> ZenityResult<Option<PathBuf>> {
let mut command = command();
command.arg("--file-selection");

add_filters(&mut command, &dialog.filters);
add_filename(&mut command, &dialog.file_name);

run(command).await.map(|res| {
res.map(|buffer| {
let trimed = buffer.trim();
trimed.into()
})
})
}

#[allow(unused)]
pub async fn pick_files(dialog: &FileDialog) -> ZenityResult<Vec<PathBuf>> {
let mut command = command();
command.args(["--file-selection", "--multiple"]);

add_filters(&mut command, &dialog.filters);
add_filename(&mut command, &dialog.file_name);

run(command).await.map(|res| {
res.map(|buffer| {
let list = buffer.trim().split('|').map(PathBuf::from).collect();
list
})
.unwrap_or(Vec::new())
})
}

#[allow(unused)]
pub async fn pick_folder(dialog: &FileDialog) -> ZenityResult<Option<PathBuf>> {
let mut command = command();
command.args(["--file-selection", "--directory"]);

add_filters(&mut command, &dialog.filters);
add_filename(&mut command, &dialog.file_name);

run(command).await.map(|res| {
res.map(|buffer| {
let trimed = buffer.trim();
trimed.into()
})
})
}

#[allow(unused)]
pub async fn save_file(dialog: &FileDialog) -> ZenityResult<Option<PathBuf>> {
let mut command = command();
command.args(["--file-selection", "--save", "--confirm-overwrite"]);

add_filters(&mut command, &dialog.filters);
add_filename(&mut command, &dialog.file_name);

run(command).await.map(|res| {
res.map(|buffer| {
let trimed = buffer.trim();
trimed.into()
})
})
}

pub async fn message(
level: &MessageLevel,
btns: &MessageButtons,
title: &str,
description: &str,
) -> ZenityResult<bool> {
let cmd = match level {
MessageLevel::Info => "--info",
MessageLevel::Warning => "--warning",
MessageLevel::Error => "--error",
};

let ok_label = match btns {
MessageButtons::Ok => None,
MessageButtons::OkCustom(ok) => Some(ok),
_ => None,
};

let mut command = command();
command.args([cmd, "--title", title, "--text", description]);

if let Some(ok) = ok_label {
command.args(["--ok-label", ok]);
}

run(command).await.map(|res| res.is_some())
}

pub async fn question(btns: &MessageButtons, title: &str, description: &str) -> ZenityResult<bool> {
let labels = match btns {
MessageButtons::OkCancel => Some(("Ok", "Cancel")),
MessageButtons::YesNo => None,
MessageButtons::OkCancelCustom(ok, cancel) => Some((ok.as_str(), cancel.as_str())),
_ => None,
};

let mut command = command();
command.args(["--question", "--title", title, "--text", description]);

if let Some((ok, cancel)) = labels {
command.args(["--ok-label", ok]);
command.args(["--cancel-label", cancel]);
}

run(command).await.map(|res| res.is_some())
}

#[cfg(test)]
mod tests {
use crate::FileDialog;

#[test]
#[ignore]
fn message() {
async_io::block_on(super::message(
&crate::message_dialog::MessageLevel::Info,
&crate::message_dialog::MessageButtons::Ok,
"hi",
"me",
))
.unwrap();
async_io::block_on(super::message(
&crate::message_dialog::MessageLevel::Warning,
&crate::message_dialog::MessageButtons::Ok,
"hi",
"me",
))
.unwrap();
async_io::block_on(super::message(
&crate::message_dialog::MessageLevel::Error,
&crate::message_dialog::MessageButtons::Ok,
"hi",
"me",
))
.unwrap();
}

#[test]
#[ignore]
fn question() {
async_io::block_on(super::question(
&crate::message_dialog::MessageButtons::OkCancel,
"hi",
"me",
))
.unwrap();
async_io::block_on(super::question(
&crate::message_dialog::MessageButtons::YesNo,
"hi",
"me",
))
.unwrap();
}

#[test]
#[ignore]
fn pick_file() {
let path = async_io::block_on(super::pick_file(&FileDialog::default())).unwrap();
dbg!(path);
}

#[test]
#[ignore]
fn pick_files() {
let path = async_io::block_on(super::pick_files(&FileDialog::default())).unwrap();
dbg!(path);
}

#[test]
#[ignore]
fn pick_folder() {
let path = async_io::block_on(super::pick_folder(&FileDialog::default())).unwrap();
dbg!(path);
}

#[test]
#[ignore]
fn save_file() {
let path = async_io::block_on(super::save_file(&FileDialog::default())).unwrap();
dbg!(path);
}
}
55 changes: 54 additions & 1 deletion src/backend/xdg_desktop_portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use std::path::PathBuf;

use crate::backend::DialogFutureType;
use crate::file_dialog::Filter;
use crate::{FileDialog, FileHandle};
use crate::message_dialog::MessageDialog;
use crate::{FileDialog, FileHandle, MessageButtons};

use ashpd::desktop::file_chooser::{
FileChooserProxy, FileFilter, OpenFileOptions, SaveFileOptions,
Expand Down Expand Up @@ -256,3 +257,55 @@ impl AsyncFileSaveDialogImpl for FileDialog {
})
}
}

use crate::backend::MessageDialogImpl;
impl MessageDialogImpl for MessageDialog {
fn show(self) -> bool {
block_on(self.show_async())
}
}

use crate::backend::AsyncMessageDialogImpl;
impl AsyncMessageDialogImpl for MessageDialog {
fn show_async(self) -> DialogFutureType<bool> {
Box::pin(async move {
match &self.buttons {
MessageButtons::Ok | MessageButtons::OkCustom(_) => {
let res = crate::backend::linux::zenity::message(
&self.level,
&self.buttons,
&self.title,
&self.description,
)
.await;

match res {
Ok(res) => res,
Err(err) => {
log::error!("Failed to open zenity dialog: {err}");
false
}
}
}
MessageButtons::OkCancel
| MessageButtons::YesNo
| MessageButtons::OkCancelCustom(_, _) => {
let res = crate::backend::linux::zenity::question(
&self.buttons,
&self.title,
&self.description,
)
.await;

match res {
Ok(res) => res,
Err(err) => {
log::error!("Failed to open zenity dialog: {err}");
false
}
}
}
}
})
}
}
Loading