rustydialogs 0.4.2

Provides a simple and cross-platform way to display native dialog boxes.
Documentation
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())
			}
		}
	}

	// Check RUSTY_DIALOGS_BACKEND env var first, then check for kdialog and zenity executables.
	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]
		};

		// Run 'which program' for each program and return the first one that exists.
		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")
	}
}