zero-dialog 0.1.0

Ultra-lightweight, dependency-free system dialog library.
Documentation
use std::ffi::CString;
use std::os::raw;
use std::{mem, ptr};

use crate::{ButtonKind, Error, IconKind, Response};

// Flags for dlopen
//   https://man7.org/linux/man-pages/man3/dlopen.3.html
const RTLD_LAZY: raw::c_int = 1;

// Flags for GtkDialog
//   https://developer.gnome.org/gtk3/stable/GtkDialog.html
const GTK_MESSAGE_INFO: i32 = 0;
const GTK_MESSAGE_WARNING: i32 = 1;
const GTK_MESSAGE_QUESTION: i32 = 2;
const GTK_MESSAGE_ERROR: i32 = 3;
const GTK_BUTTONS_OK: i32 = 1;
// const GTK_BUTTONS_CLOSE: i32 = 2;
// const GTK_BUTTONS_CANCEL: i32 = 3;
// const GTK_BUTTONS_YES_NO: i32 = 4;
const GTK_BUTTONS_OK_CANCEL: i32 = 5;
const GTK_RESPONSE_OK: i32 = -5;
const GTK_RESPONSE_CANCEL: i32 = -6;

// Dl functions
#[cfg_attr(any(target_os = "linux", target_os = "android"), link(name = "dl"))]
#[cfg_attr(any(target_os = "freebsd", target_os = "dragonfly"), link(name = "c"))]
unsafe extern "C" {
    unsafe fn dlopen(filename: *const raw::c_char, flags: raw::c_int) -> *mut raw::c_void;
    unsafe fn dlclose(handle: *mut raw::c_void) -> raw::c_int;
    unsafe fn dlsym(handle: *mut raw::c_void, symbol: *const raw::c_char) -> *mut raw::c_void;
    // unsafe fn dlerror() -> *mut raw::c_char;
}

// GTK functions
type GtkInit = unsafe extern "C" fn(argc: *mut raw::c_int, argv: *mut *mut *mut raw::c_char);
type GtkMessageDialogNew = unsafe extern "C" fn(
    parent: *mut raw::c_void,
    flags: raw::c_int,
    message_type: raw::c_int,
    buttons_type: raw::c_int,
    msg: *const raw::c_char,
    ...
) -> *mut raw::c_void;
type GtkWindowSetTitle = unsafe extern "C" fn(window: *mut raw::c_void, title: *const raw::c_char);
type GtkWidgetShow = unsafe extern "C" fn(widget: *mut raw::c_void);
type GtkDialogRun = unsafe extern "C" fn(dialog: *mut raw::c_void) -> raw::c_int;
type GtkWidgetDestroy = unsafe extern "C" fn(widget: *mut raw::c_void);

/// Convert `IconKind`` to `GTK_MESSAGE_TYPE`
fn to_gtk_message_type(icon: IconKind) -> i32 {
    match icon {
        IconKind::Info => GTK_MESSAGE_INFO,
        IconKind::Warning => GTK_MESSAGE_WARNING,
        IconKind::Error => GTK_MESSAGE_ERROR,
        IconKind::Question => GTK_MESSAGE_QUESTION,
    }
}

/// Convert `ButtonKind` to `GTK_BUTTONS_TYPE`
fn to_gtk_buttons_type(btn: ButtonKind) -> i32 {
    match btn {
        ButtonKind::Ok => GTK_BUTTONS_OK,
        ButtonKind::OkCancel => GTK_BUTTONS_OK_CANCEL,
    }
}

pub fn gtk3_show_dialog(
    title: &str,
    message: &str,
    icon: IconKind,
    btn: ButtonKind,
) -> Result<Response, Error> {
    // try to load libgtk
    let lib_names = [
        // "libgtk-4.so.1",       // GTK 4
        // "libgtk-x11-2.0.so.0", // GTK 2
        "libgtk-3.so.0", // GTK 3
    ];

    let mut lib_handle = ptr::null_mut();
    let mut found = false;

    for lib_name in lib_names.iter() {
        let cname = CString::new(*lib_name).map_err(|_| Error::InvalidString)?;
        unsafe {
            lib_handle = dlopen(cname.as_ptr(), RTLD_LAZY);
            if !lib_handle.is_null() {
                found = true;
                break;
            }
        }
    }

    if !found {
        return Err(Error::FailedToLoadLibrary);
    }

    // find symbols
    unsafe {
        let init_sym = dlsym(lib_handle, b"gtk_init\0".as_ptr() as *const _);
        let dialog_new_sym = dlsym(lib_handle, b"gtk_message_dialog_new\0".as_ptr() as *const _);
        let set_title_sym = dlsym(lib_handle, b"gtk_window_set_title\0".as_ptr() as *const _);
        let widget_show_sym = dlsym(lib_handle, b"gtk_widget_show\0".as_ptr() as *const _);
        let dialog_run_sym = dlsym(lib_handle, b"gtk_dialog_run\0".as_ptr() as *const _);
        let widget_destroy_sym = dlsym(lib_handle, b"gtk_widget_destroy\0".as_ptr() as *const _);

        if init_sym.is_null()
            || dialog_new_sym.is_null()
            || set_title_sym.is_null()
            || widget_show_sym.is_null()
            || dialog_run_sym.is_null()
            || widget_destroy_sym.is_null()
        {
            dlclose(lib_handle);
            return Err(Error::FailedToFindSymbol);
        }

        let gtk_init: GtkInit = mem::transmute(init_sym);
        let gtk_message_dialog_new: GtkMessageDialogNew = mem::transmute(dialog_new_sym);
        let gtk_window_set_title: GtkWindowSetTitle = mem::transmute(set_title_sym);
        let gtk_widget_show: GtkWidgetShow = mem::transmute(widget_show_sym);
        let gtk_dialog_run: GtkDialogRun = mem::transmute(dialog_run_sym);
        let gtk_widget_destroy: GtkWidgetDestroy = mem::transmute(widget_destroy_sym);

        gtk_init(ptr::null_mut(), ptr::null_mut());

        let c_title = CString::new(title).map_err(|_| Error::InvalidString)?;
        let c_message = CString::new(message).map_err(|_| Error::InvalidString)?;

        let icon = to_gtk_message_type(icon);
        let buttons = to_gtk_buttons_type(btn);
        let dialog = gtk_message_dialog_new(
            ptr::null_mut(),                        // without parent
            0,                                      // no flags
            icon,                                   // set message type
            buttons,                                // set buttons type
            b"%s\0".as_ptr() as *const raw::c_char, // format
            c_message.as_ptr(),                     // message
        );

        gtk_window_set_title(dialog as *mut raw::c_void, c_title.as_ptr());
        gtk_widget_show(dialog);
        let ret = gtk_dialog_run(dialog as *mut raw::c_void) as i32;
        gtk_widget_destroy(dialog);
        dlclose(lib_handle);

        match ret {
            GTK_RESPONSE_OK => Ok(Response::Ok),
            GTK_RESPONSE_CANCEL => Ok(Response::Cancel),
            _ => Ok(Response::None),
        }
    }
}