use std::{path, process, str, sync};
use std::ffi::OsStr;
use std::fmt::Write;
use std::os::unix::ffi::OsStrExt;
use super::*;
mod kdialog;
mod zenity;
#[cfg(feature = "libnotify")]
mod notify;
#[cfg(feature = "xdg-portal")]
mod xdg_portal;
#[cfg(feature = "gtk3")]
mod gtk3;
#[cfg(feature = "gtk4")]
mod gtk4;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Backend {
KDialog,
Zenity,
#[cfg(feature = "xdg-portal")]
XdgPortal,
#[cfg(feature = "gtk3")]
Gtk3,
#[cfg(feature = "gtk4")]
Gtk4,
}
static BACKEND: sync::LazyLock<Backend> = sync::LazyLock::new(|| {
fn getenv(key: &std::ffi::CStr) -> Option<&[u8]> {
unsafe {
let ptr = libc::getenv(key.as_ptr());
if ptr.is_null() {
None
}
else {
Some(std::ffi::CStr::from_ptr(ptr).to_bytes())
}
}
}
if let Some(backend) = getenv(c"RUSTY_DIALOGS_BACKEND") {
match backend {
b"kdialog" => return Backend::KDialog,
b"zenity" => return Backend::Zenity,
#[cfg(feature = "xdg-portal")]
b"xdg-portal" => return Backend::XdgPortal,
#[cfg(feature = "gtk4")]
b"gtk4" => return Backend::Gtk4,
#[cfg(feature = "gtk3")]
b"gtk3" => return Backend::Gtk3,
_ => panic!("Invalid RUSTY_DIALOGS_BACKEND value: {backend:?}", backend = str::from_utf8(backend).unwrap_or("<invalid utf-8>")),
}
}
#[allow(unreachable_code)]
#[cfg(feature = "gtk4")] {
return Backend::Gtk4;
}
#[allow(unreachable_code)]
#[cfg(feature = "gtk3")] {
return Backend::Gtk3;
}
#[allow(unreachable_code)] {
fn isenv(key: &std::ffi::CStr) -> bool {
unsafe { !libc::getenv(key.as_ptr()).is_null() }
}
let desktop = getenv(c"XDG_CURRENT_DESKTOP").or_else(|| getenv(c"DESKTOP_SESSION")).and_then(|s| str::from_utf8(s).ok());
let preferred_programs = if let Some(desktop) = desktop {
if desktop.contains("gnome") {
[Backend::Zenity, Backend::KDialog]
}
else if desktop.contains("kde") || desktop.contains("plasma") {
[Backend::KDialog, Backend::Zenity]
}
else {
[Backend::Zenity, Backend::KDialog]
}
}
else if isenv(c"GNOME_DESKTOP_SESSION_ID") {
[Backend::Zenity, Backend::KDialog]
}
else {
[Backend::KDialog, Backend::Zenity]
};
for &backend in &preferred_programs {
let program = match backend {
Backend::KDialog => "kdialog",
Backend::Zenity => "zenity",
#[allow(unreachable_patterns)]
_ => continue,
};
if process::Command::new("which").arg(program).output().map(|output| output.status.success()).unwrap_or(false) {
return backend;
}
}
panic!("No supported dialog backend found. Please install kdialog or zenity, or set RUSTY_DIALOGS_BACKEND to a supported backend.");
}
});
pub fn message_box(p: &MessageBox<'_>) -> Option<MessageResult> {
match *BACKEND {
Backend::KDialog => kdialog::message_box(p),
Backend::Zenity => zenity::message_box(p),
#[cfg(feature = "xdg-portal")]
Backend::XdgPortal => xdg_portal::message_box(p),
#[cfg(feature = "gtk3")]
Backend::Gtk3 => gtk3::message_box(p),
#[cfg(feature = "gtk4")]
Backend::Gtk4 => gtk4::message_box(p),
}
}
pub fn pick_file(p: &FileDialog<'_>) -> Option<path::PathBuf> {
match *BACKEND {
Backend::KDialog => kdialog::pick_file(p),
Backend::Zenity => zenity::pick_file(p),
#[cfg(feature = "xdg-portal")]
Backend::XdgPortal => xdg_portal::pick_file(p),
#[cfg(feature = "gtk3")]
Backend::Gtk3 => gtk3::pick_file(p),
#[cfg(feature = "gtk4")]
Backend::Gtk4 => gtk4::pick_file(p),
}
}
pub fn pick_files(p: &FileDialog<'_>) -> Option<Vec<path::PathBuf>> {
match *BACKEND {
Backend::KDialog => kdialog::pick_files(p),
Backend::Zenity => zenity::pick_files(p),
#[cfg(feature = "xdg-portal")]
Backend::XdgPortal => xdg_portal::pick_files(p),
#[cfg(feature = "gtk3")]
Backend::Gtk3 => gtk3::pick_files(p),
#[cfg(feature = "gtk4")]
Backend::Gtk4 => gtk4::pick_files(p),
}
}
pub fn save_file(p: &FileDialog<'_>) -> Option<path::PathBuf> {
match *BACKEND {
Backend::KDialog => kdialog::save_file(p),
Backend::Zenity => zenity::save_file(p),
#[cfg(feature = "xdg-portal")]
Backend::XdgPortal => xdg_portal::save_file(p),
#[cfg(feature = "gtk3")]
Backend::Gtk3 => gtk3::save_file(p),
#[cfg(feature = "gtk4")]
Backend::Gtk4 => gtk4::save_file(p),
}
}
pub fn folder_dialog(p: &FolderDialog<'_>) -> Option<path::PathBuf> {
match *BACKEND {
Backend::KDialog => kdialog::folder_dialog(p),
Backend::Zenity => zenity::folder_dialog(p),
#[cfg(feature = "xdg-portal")]
Backend::XdgPortal => xdg_portal::folder_dialog(p),
#[cfg(feature = "gtk3")]
Backend::Gtk3 => gtk3::folder_dialog(p),
#[cfg(feature = "gtk4")]
Backend::Gtk4 => gtk4::folder_dialog(p),
}
}
pub fn text_input(p: &TextInput<'_>) -> Option<String> {
match *BACKEND {
Backend::KDialog => kdialog::text_input(p),
Backend::Zenity => zenity::text_input(p),
#[cfg(feature = "xdg-portal")]
Backend::XdgPortal => xdg_portal::text_input(p),
#[cfg(feature = "gtk3")]
Backend::Gtk3 => gtk3::text_input(p),
#[cfg(feature = "gtk4")]
Backend::Gtk4 => gtk4::text_input(p),
}
}
pub fn color_picker(p: &ColorPicker<'_>) -> Option<ColorValue> {
match *BACKEND {
Backend::KDialog => kdialog::color_picker(p),
Backend::Zenity => zenity::color_picker(p),
#[cfg(feature = "xdg-portal")]
Backend::XdgPortal => xdg_portal::color_picker(p),
#[cfg(feature = "gtk3")]
Backend::Gtk3 => gtk3::color_picker(p),
#[cfg(feature = "gtk4")]
Backend::Gtk4 => gtk4::color_picker(p),
}
}
#[inline]
pub fn notify_setup(app_id: &str) -> bool {
#[cfg(feature = "libnotify")] {
notify::init(app_id)
}
#[cfg(not(feature = "libnotify"))] {
!app_id.is_empty()
}
}
pub fn notify(p: &Notification<'_>) {
if p.app_id.is_empty() {
return;
}
match *BACKEND {
#[cfg(feature = "libnotify")]
Backend::KDialog => notify::notify(p),
#[cfg(not(feature = "libnotify"))]
Backend::KDialog => kdialog::notify(p),
#[cfg(feature = "libnotify")]
Backend::Zenity => notify::notify(p),
#[cfg(not(feature = "libnotify"))]
Backend::Zenity => zenity::notify(p),
#[cfg(feature = "xdg-portal")]
Backend::XdgPortal => xdg_portal::notify(p),
#[cfg(feature = "gtk3")]
Backend::Gtk3 => notify::notify(p),
#[cfg(feature = "gtk4")]
Backend::Gtk4 => notify::notify(p),
}
}
#[inline]
fn os(s: &str) -> &OsStr {
OsStr::new(s)
}
#[track_caller]
fn invoke(program: &str, args: &[&OsStr]) -> Option<i32> {
let mut child = process::Command::new(program).args(args).spawn().expect("failed to spawn process");
child.wait().expect("failed to wait for process").code()
}
#[track_caller]
fn invoke_async(program: &str, args: &[&OsStr]) {
let _ = process::Command::new(program).args(args).spawn().expect("failed to spawn process");
}
#[track_caller]
fn invoke_output(program: &str, args: &[&OsStr]) -> (Option<i32>, String) {
let output = process::Command::new(program).args(args).output().expect("failed to spawn process");
let mut stdout = String::from_utf8(output.stdout).expect("failed to parse stdout as UTF-8");
if stdout.ends_with('\n') {
stdout.pop();
}
let code = output.status.code();
(code, stdout)
}
#[track_caller]
fn invoke_output_bytes(program: &str, args: &[&OsStr]) -> (Option<i32>, Vec<u8>) {
let output = process::Command::new(program).args(args).output().expect("failed to spawn process");
(output.status.code(), output.stdout)
}
#[track_caller]
fn exit_status_error(status: Option<i32>) -> ! {
if let Some(code) = status {
panic!("terminated with exit code: {code}")
}
else {
panic!("terminated without an exit code")
}
}