use super::{gtk_sudo, CursorData, ResultType};
use desktop::Desktop;
pub use hbb_common::platform::linux::*;

#[cfg(feature = "drm")]
pub fn dispatch_wayland_display_probe() {
    use std::ffi::OsStr;

    if std::env::args_os().nth(1).as_deref() == Some(OsStr::new(WAYLAND_DISPLAY_PROBE_ARG)) {
        wayland_display_probe_child_main();
    }
}
use hbb_common::{
    allow_err,
    anyhow::anyhow,
    bail,
    config::Config,
    libc::{c_char, c_int, c_long, c_uint, c_ulong, c_void},
    log,
    message_proto::{DisplayInfo, Resolution},
    regex::{Captures, Regex},
    users::{get_user_by_name, os::unix::UserExt},
};
use libxdo_sys::{self, xdo_t, Window};
use std::{
    cell::RefCell,
    ffi::{OsStr, OsString},
    path::{Path, PathBuf},
    process::{Child, Command},
    string::String,
    sync::atomic::{AtomicBool, Ordering},
    sync::Arc,
    time::{Duration, Instant},
};
use terminfo::{capability as cap, Database};
use wallpaper;

pub const PA_SAMPLE_RATE: u32 = 48000;
static mut UNMODIFIED: bool = true;

#[derive(Clone, Debug)]
struct ActiveUserLookupCache {
    uid: String,
    username: String,
}

const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"];
const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"];

// Terminal type constants
const TERM_XTERM_256COLOR: &str = "xterm-256color";
const TERM_SCREEN_256COLOR: &str = "screen-256color";
const TERM_XTERM: &str = "xterm";

#[cfg(feature = "drm")]
lazy_static::lazy_static! {
    /// Only for per-frame callers; see `is_login_screen_wayland_cached`.
    /// Own block because `#[cfg]` on one item inside a shared one breaks the macro.
    static ref IS_LOGIN_SCREEN_WAYLAND: bool = is_login_screen_wayland();
}

lazy_static::lazy_static! {
    /// `is_x11_or_headless()` answers x11 at a Wayland greeter, which the portal could not
    /// serve but the DRM path can. Unmemoised lookup on purpose: this may run mid-boot, and
    /// a "no" cached that early would be wrong for the rest of the process.
    pub static ref IS_X11: bool = {
        let x11 = hbb_common::platform::linux::is_x11_or_headless();
        #[cfg(feature = "drm")]
        {
            if x11 && !display_server_forced() && is_login_screen_wayland() {
                log::info!(
                    "drm: seat0 is a Wayland login screen that reads as x11 upstream; \
                     treating it as Wayland so the DRM path is not disabled at the one \
                     screen it exists for"
                );
                false
            } else {
                x11
            }
        }
        #[cfg(not(feature = "drm"))]
        {
            x11
        }
    };
    // Cache for TERM value - once TERM_XTERM_256COLOR is found, reuse it directly
    static ref CACHED_TERM: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
    static ref DATABASE_XTERM_256COLOR: Option<Database> = {
        match Database::from_name(TERM_XTERM_256COLOR) {
            Ok(database) => Some(database),
            Err(err) => {
                log::error!("Failed to initialize {} database: {}", TERM_XTERM_256COLOR, err);
                None
            }
        }
    };
    static ref ACTIVE_USER_LOOKUP_CACHE: std::sync::Mutex<Option<ActiveUserLookupCache>> =
        std::sync::Mutex::new(None);
    static ref GNOME_MONITOR_LAYOUT_MODE_CACHE: std::sync::Mutex<
        Option<(Instant, Option<GnomeMonitorLayoutMode>)>,
    > = Default::default();
    // https://github.com/rustdesk/rustdesk/issues/13705
    // Check if `sudo -E` actually preserves environment.
    //
    // This flag is only used by `run_as_user()` (root service -> user session). If the current process is not
    // running as `root`, this check is meaningless (and `sudo -n` may fail), so we return `false` directly.
    //
    // On Ubuntu 25.10, `sudo -E` may still succeed but effectively ignores `-E`. Some versions print a warning
    // to stderr (wording may vary by locale), so we verify behavior instead:
    // - Inject a sentinel environment variable into the `sudo` process
    // - Run `sudo -n -E env` and check whether the sentinel is present in stdout
    static ref SUDO_E_PRESERVES_ENV: bool = {
        if !is_root() {
            log::warn!("Not running as root, SUDO_E_PRESERVES_ENV check skipped");
            false
        } else {
            let key = format!("__RUSTDESK_SUDO_E_TEST_{}", std::process::id());
            let val = "1";
            let expected = format!("{key}={val}");
            Command::new("sudo")
                // -n for non-interactive to avoid password prompt
                .env(&key, val)
                .args(["-n", "-E", "env"])
                .output()
                .map(|o| {
                    o.status.success()
                        && String::from_utf8_lossy(&o.stdout).contains(expected.as_str())
                })
                .unwrap_or(false)
        }
    };
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GnomeMonitorLayoutMode {
    Logical,
    Physical,
}

impl GnomeMonitorLayoutMode {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Logical => "logical",
            Self::Physical => "physical",
        }
    }
}

fn gnome_monitor_layout_mode_from_value(value: u32) -> Option<GnomeMonitorLayoutMode> {
    // Upstream: https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml
    // Ubuntu mode 3: https://git.launchpad.net/ubuntu/+source/mutter/tree/debian/patches/x11-Add-support-for-fractional-scaling-using-Randr.patch
    match value {
        1 | 3 => Some(GnomeMonitorLayoutMode::Logical),
        2 => Some(GnomeMonitorLayoutMode::Physical),
        _ => None,
    }
}

pub fn gnome_monitor_layout_mode() -> Option<GnomeMonitorLayoutMode> {
    if let Ok(cache) = GNOME_MONITOR_LAYOUT_MODE_CACHE.lock() {
        if let Some((updated_at, result)) = *cache {
            if updated_at.elapsed() < Duration::from_secs(10) {
                return result;
            }
        }
    }

    let result = (|| {
        let is_gnome_desktop = std::env::var("XDG_CURRENT_DESKTOP")
            .unwrap_or_default()
            .split(':')
            .any(|desktop| {
                desktop.eq_ignore_ascii_case("gnome") || desktop.eq_ignore_ascii_case("unity")
            });
        let is_gnome_session = std::env::var("DESKTOP_SESSION")
            .unwrap_or_default()
            .to_ascii_lowercase();
        if !is_gnome_desktop && !is_gnome_session.contains("gnome") {
            return None;
        }
        use dbus::{arg::PropMap, blocking::BlockingSender};

        let conn = match dbus::blocking::Connection::new_session() {
            Ok(conn) => conn,
            Err(err) => {
                log::warn!("Failed to connect to the session bus for GNOME monitor layout: {err}");
                return None;
            }
        };
        let message = match dbus::Message::new_method_call(
            "org.gnome.Mutter.DisplayConfig",
            "/org/gnome/Mutter/DisplayConfig",
            "org.gnome.Mutter.DisplayConfig",
            "GetCurrentState",
        ) {
            Ok(message) => message,
            Err(err) => {
                log::warn!("Failed to create GNOME monitor layout query: {err}");
                return None;
            }
        };
        let reply = match conn.send_with_reply_and_block(message, Duration::from_secs(2)) {
            Ok(reply) => reply,
            Err(err) => {
                log::warn!("Failed to query GNOME monitor layout: {err}");
                return None;
            }
        };
        let mut args = reply.iter_init();
        for _ in 0..3 {
            if !args.next() {
                log::warn!("GNOME monitor layout reply is missing properties");
                return None;
            }
        }
        let properties: PropMap = match args.read() {
            Ok(properties) => properties,
            Err(err) => {
                log::warn!("Failed to read GNOME monitor layout properties: {err}");
                return None;
            }
        };
        let Some(value) = dbus::arg::prop_cast::<u32>(&properties, "layout-mode").copied() else {
            log::warn!("GNOME monitor layout reply has no layout-mode");
            return None;
        };
        let mode = gnome_monitor_layout_mode_from_value(value);
        if mode.is_none() {
            log::warn!("GNOME monitor layout reply has unknown layout-mode {value}");
        }
        mode
    })();
    if let Ok(mut cache) = GNOME_MONITOR_LAYOUT_MODE_CACHE.lock() {
        *cache = Some((Instant::now(), result));
    }
    result
}

#[cfg(test)]
mod gnome_monitor_layout_tests {
    use super::*;

    #[test]
    fn maps_logical_layouts() {
        assert_eq!(
            gnome_monitor_layout_mode_from_value(1),
            Some(GnomeMonitorLayoutMode::Logical)
        );
        assert_eq!(
            gnome_monitor_layout_mode_from_value(3),
            Some(GnomeMonitorLayoutMode::Logical)
        );
    }

    #[test]
    fn maps_physical_layout() {
        assert_eq!(
            gnome_monitor_layout_mode_from_value(2),
            Some(GnomeMonitorLayoutMode::Physical)
        );
    }

    #[test]
    fn rejects_unknown_layout() {
        assert_eq!(gnome_monitor_layout_mode_from_value(4), None);
    }
}

#[inline]
fn update_active_user_lookup_cache(desktop: &Desktop) {
    if let Ok(mut cache) = ACTIVE_USER_LOOKUP_CACHE.lock() {
        if desktop.uid.is_empty() || desktop.username.is_empty() {
            *cache = None;
        } else {
            *cache = Some(ActiveUserLookupCache {
                uid: desktop.uid.clone(),
                username: desktop.username.clone(),
            });
        }
    }
}

#[inline]
fn get_active_user_id_name_from_cache() -> Option<(String, String)> {
    let cache = ACTIVE_USER_LOOKUP_CACHE.lock().ok()?;
    let entry = cache.as_ref()?;
    Some((entry.uid.clone(), entry.username.clone()))
}

thread_local! {
    // XDO context - created via libxdo-sys (which uses dynamic loading stub).
    // If libxdo is not available, xdo will be null and xdo-based functions become no-ops.
    static XDO: RefCell<*mut xdo_t> = RefCell::new({
        let xdo = unsafe { libxdo_sys::xdo_new(std::ptr::null()) };
        if xdo.is_null() {
            log::warn!("Failed to create xdo context, xdo functions will be disabled");
        } else {
            log::info!("xdo context created successfully");
        }
        xdo
    });
    static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())});
}

// X11 error event structure for the custom error handler.
// See: https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Using-the-Default-Error-Handlers
#[repr(C)]
struct XErrorEvent {
    type_: c_int,
    display: *mut c_void, // Display*
    resourceid: c_ulong,  // XID
    serial: c_ulong,
    error_code: u8,
    request_code: u8,
    minor_code: u8,
}

type XErrorHandler = unsafe extern "C" fn(*mut c_void, *mut XErrorEvent) -> c_int;

const X11_BAD_WINDOW: u8 = 3;
const XDO_SUCCESS: c_int = 0;
const XDO_ERROR: c_int = 1;

/// Atomic flag set by the custom X error handler when a BadWindow error occurs.
static X_BAD_WINDOW_DETECTED: AtomicBool = AtomicBool::new(false);
static X_UNEXPECTED_ERROR_DETECTED: AtomicBool = AtomicBool::new(false);

/// Custom X error handler that catches BadWindow errors (error_code == 3) instead of
/// letting the default handler terminate the process.
/// See issue: https://github.com/rustdesk/rustdesk/issues/9003
unsafe extern "C" fn handle_x_error(_display: *mut c_void, event: *mut XErrorEvent) -> c_int {
    if !event.is_null() && (*event).error_code == X11_BAD_WINDOW {
        X_BAD_WINDOW_DETECTED.store(true, Ordering::SeqCst);
        log::debug!("Caught X11 BadWindow error (suppressed), window was likely destroyed");
        return 0;
    }
    X_UNEXPECTED_ERROR_DETECTED.store(true, Ordering::SeqCst);
    if !event.is_null() {
        log::warn!(
            "X11 error: error_code={}, request_code={}, minor_code={}",
            (*event).error_code,
            (*event).request_code,
            (*event).minor_code,
        );
    }
    0
}

#[link(name = "X11")]
extern "C" {
    fn XOpenDisplay(display_name: *const c_char) -> *mut c_void;
    // fn XCloseDisplay(d: *mut c_void) -> c_int;
    fn XSetErrorHandler(handler: Option<XErrorHandler>) -> Option<XErrorHandler>;
}

#[link(name = "Xfixes")]
extern "C" {
    // fn XFixesQueryExtension(dpy: *mut c_void, event: *mut c_int, error: *mut c_int) -> c_int;
    fn XFixesGetCursorImage(dpy: *mut c_void) -> *const xcb_xfixes_get_cursor_image;
    fn XFree(data: *mut c_void);
}

// /usr/include/X11/extensions/Xfixes.h
#[repr(C)]
pub struct xcb_xfixes_get_cursor_image {
    pub x: i16,
    pub y: i16,
    pub width: u16,
    pub height: u16,
    pub xhot: u16,
    pub yhot: u16,
    pub cursor_serial: c_long,
    pub pixels: *const c_long,
}

#[inline]
pub fn is_login_screen_wayland() -> bool {
    let values = get_values_of_seat0_with_gdm_wayland(&[0, 2]);
    is_gdm_user(&values[1]) && get_display_server_of_session(&values[0]) == DISPLAY_SERVER_WAYLAND
}

/// An explicit `RUSTDESK_FORCED_DISPLAY_SERVER` is an operator override, and the root service
/// forwards it to the per-user server on purpose: the greeter correction may only fix an
/// AUTO-detected answer, never argue with the operator — a half-applied override would leave
/// `get_display_server()` and the DRM routing gates disagreeing with each other.
#[cfg(feature = "drm")]
pub(crate) fn display_server_forced() -> bool {
    std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER").is_ok()
}

/// X11 as far as the DRM path is concerned: a Wayland greeter is not, unless the operator
/// forced the display server.
///
/// Both halves unmemoised, for the retry loops that must keep asking until seat0 can be named.
#[cfg(feature = "drm")]
pub fn is_x11_for_drm() -> bool {
    scrap::is_x11() && (display_server_forced() || !is_login_screen_wayland())
}

/// Memoised `is_login_screen_wayland`, for per-frame callers that must not run `loginctl`.
///
/// Only from the per-session `--server`: it is spawned after the session is identified, so the
/// answer is settled. Anything that can run mid-boot must use the uncached form.
#[cfg(feature = "drm")]
#[inline]
pub fn is_login_screen_wayland_cached() -> bool {
    *IS_LOGIN_SCREEN_WAYLAND
}

#[inline]
fn sleep_millis(millis: u64) {
    std::thread::sleep(Duration::from_millis(millis));
}

pub fn get_cursor_pos() -> Option<(i32, i32)> {
    let mut res = None;
    XDO.with(|xdo| {
        if let Ok(xdo) = xdo.try_borrow() {
            if xdo.is_null() {
                return;
            }
            let mut x: c_int = 0;
            let mut y: c_int = 0;
            unsafe {
                libxdo_sys::xdo_get_mouse_location(
                    *xdo as *const _,
                    &mut x as _,
                    &mut y as _,
                    std::ptr::null_mut(),
                );
            }
            res = Some((x, y));
        }
    });
    res
}

pub fn set_cursor_pos(x: i32, y: i32) -> bool {
    let mut res = false;
    XDO.with(|xdo| {
        match xdo.try_borrow() {
            Ok(xdo) => {
                if xdo.is_null() {
                    log::debug!("set_cursor_pos: xdo is null");
                    return;
                }
                unsafe {
                    let ret = libxdo_sys::xdo_move_mouse(*xdo as *const _, x, y, 0);
                    if ret != 0 {
                        log::debug!(
                            "set_cursor_pos: xdo_move_mouse failed with code {} for coordinates ({}, {})",
                            ret, x, y
                        );
                    }
                    res = ret == 0;
                }
            }
            Err(_) => {
                log::debug!("set_cursor_pos: failed to borrow xdo");
            }
        }
    });
    res
}

/// Clip cursor - Linux implementation is a no-op.
///
/// On X11, there's no direct equivalent to Windows ClipCursor. XGrabPointer
/// can confine the pointer but requires a window handle and has side effects.
///
/// On Wayland, pointer constraints require the zwp_pointer_constraints_v1
/// protocol which is compositor-dependent.
///
/// For relative mouse mode on Linux, the Flutter side uses pointer warping
/// (set_cursor_pos) to re-center the cursor after each movement, which achieves
/// a similar effect without requiring cursor clipping.
///
/// Returns true (always succeeds as no-op).
pub fn clip_cursor(_rect: Option<(i32, i32, i32, i32)>) -> bool {
    // Log only once per process to avoid flooding logs when called frequently.
    static LOGGED: AtomicBool = AtomicBool::new(false);
    if !LOGGED.swap(true, Ordering::Relaxed) {
        log::debug!("clip_cursor called (no-op on Linux, this message is logged only once)");
    }
    true
}

pub fn reset_input_cache() {}

pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
    let mut res = None;
    XDO.with(|xdo| {
        if let Ok(xdo) = xdo.try_borrow() {
            if xdo.is_null() {
                return;
            }
            let mut x: c_int = 0;
            let mut y: c_int = 0;
            let mut width: c_uint = 0;
            let mut height: c_uint = 0;
            let mut window: Window = 0;

            unsafe {
                if libxdo_sys::xdo_get_active_window(*xdo as *const _, &mut window) != 0 {
                    return;
                }

                // XSetErrorHandler is process-global, not scoped to this Display/thread.
                // This path is currently called by the single window_focus service thread.
                // While installed, this handler can still observe unrelated X11 errors from
                // other threads; unexpected errors make this geometry query fail.
                X_BAD_WINDOW_DETECTED.store(false, Ordering::SeqCst);
                X_UNEXPECTED_ERROR_DETECTED.store(false, Ordering::SeqCst);
                let prev_handler = XSetErrorHandler(Some(handle_x_error));

                let loc_ret = libxdo_sys::xdo_get_window_location(
                    *xdo as *const _,
                    window,
                    &mut x as _,
                    &mut y as _,
                    std::ptr::null_mut(),
                );
                let size_ret = if loc_ret == XDO_SUCCESS {
                    libxdo_sys::xdo_get_window_size(
                        *xdo as *const _,
                        window,
                        &mut width,
                        &mut height,
                    )
                } else {
                    XDO_ERROR
                };

                // Do not call XSync(DISPLAY) here: DISPLAY is a separate
                // XOpenDisplay() connection, while libxdo owns the Display*
                // used by these geometry queries. These libxdo calls are
                // synchronous XGetWindowAttributes-based queries, so the target
                // BadWindow is expected to be delivered before the calls return.
                XSetErrorHandler(prev_handler);
                if X_BAD_WINDOW_DETECTED.load(Ordering::SeqCst)
                    || X_UNEXPECTED_ERROR_DETECTED.load(Ordering::SeqCst)
                    || loc_ret != XDO_SUCCESS
                    || size_ret != XDO_SUCCESS
                {
                    return;
                }

                let center_x = x + (width / 2) as c_int;
                let center_y = y + (height / 2) as c_int;
                res = displays.iter().position(|d| {
                    center_x >= d.x
                        && center_x < d.x + d.width
                        && center_y >= d.y
                        && center_y < d.y + d.height
                });
            }
        }
    });
    res
}

pub fn get_cursor() -> ResultType<Option<u64>> {
    // DRM/KMS capture: the hardware cursor arrives over the `_drm` stream, not from XFixes.
    //
    // The MEMOISED `is_x11()` here, deliberately, unlike the capture-path callers that take the
    // unmemoised `scrap::is_x11()` because this one latches on first use. The tradeoff is the other
    // way round at cursor cadence: the unmemoised form forks `loginctl` per call, and this runs on
    // every cursor poll. A latch that guessed wrong costs a cursor served by the wrong source until
    // the process restarts, not a capture that cannot start -- and by the time a cursor is being
    // polled there is a live session, which is the case the latch reads correctly.
    #[cfg(feature = "drm")]
    if !is_x11() {
        if let Some(id) = crate::server::drm_capturer::drm_cursor_id() {
            // In a mixed DRM + PipeWire session the DRM streams only cover the DRM-backed displays;
            // when the pointer sits on a PipeWire-served display every DRM stream reports the hidden
            // sentinel. Returning that sentinel here would hide the cursor globally, including on the
            // PipeWire display where it is still visible, so only report a hidden DRM cursor when it
            // is authoritative -- a pure-DRM session. A visible DRM cursor is always authoritative;
            // otherwise fall through to the normal cursor path.
            if id != scrap::drm_reader::HIDDEN_CURSOR_ID
                || !crate::server::display_service::has_non_drm_backed_display()
            {
                return Ok(Some(id));
            }
        }
    }
    let mut res = None;
    DISPLAY.with(|conn| {
        if let Ok(d) = conn.try_borrow_mut() {
            if !d.is_null() {
                unsafe {
                    let img = XFixesGetCursorImage(*d);
                    if !img.is_null() {
                        res = Some((*img).cursor_serial as u64);
                        XFree(img as _);
                    }
                }
            }
        }
    });
    Ok(res)
}

pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
    // DRM/KMS capture: return the latest hardware-cursor snapshot from the `_drm` stream. Its id may
    // have advanced past `hcursor` between get_cursor() and here, so return the latest rather than
    // bailing (which would trigger a MouseCursorService backoff).
    //
    // Memoised `is_x11()` on purpose, for the reason spelled out in `get_cursor()`; the two must
    // agree anyway, since a caller that took the DRM branch there has to take it here.
    #[cfg(feature = "drm")]
    if !is_x11() {
        if let Some(c) = crate::server::drm_capturer::drm_cursor() {
            // See get_cursor(): a hidden DRM sentinel is authoritative only in a pure-DRM session. In
            // a mixed DRM + PipeWire session fall through so the PipeWire display's cursor is served
            // by the normal path instead of being hidden everywhere.
            if c.id != scrap::drm_reader::HIDDEN_CURSOR_ID
                || !crate::server::display_service::has_non_drm_backed_display()
            {
                let mut cd: CursorData = Default::default();
                cd.id = c.id;
                cd.width = c.width;
                cd.height = c.height;
                cd.hotx = c.hotx;
                cd.hoty = c.hoty;
                cd.colors = c.colors.into();
                return Ok(cd);
            }
        }
    }
    let mut res = None;
    DISPLAY.with(|conn| {
        if let Ok(ref mut d) = conn.try_borrow_mut() {
            if !d.is_null() {
                unsafe {
                    let img = XFixesGetCursorImage(**d);
                    if !img.is_null() && hcursor == (*img).cursor_serial as u64 {
                        let mut cd: CursorData = Default::default();
                        cd.hotx = (*img).xhot as _;
                        cd.hoty = (*img).yhot as _;
                        cd.width = (*img).width as _;
                        cd.height = (*img).height as _;
                        // to-do: how about if it is 0
                        cd.id = (*img).cursor_serial as _;
                        let pixels =
                            std::slice::from_raw_parts((*img).pixels, (cd.width * cd.height) as _);
                        // cd.colors.resize(pixels.len() * 4, 0);
                        let mut cd_colors = vec![0_u8; pixels.len() * 4];
                        for y in 0..cd.height {
                            for x in 0..cd.width {
                                let pos = (y * cd.width + x) as usize;
                                let p = pixels[pos];
                                let a = (p >> 24) & 0xff;
                                let r = (p >> 16) & 0xff;
                                let g = (p >> 8) & 0xff;
                                let b = (p >> 0) & 0xff;
                                if a == 0 {
                                    continue;
                                }
                                let pos = pos * 4;
                                cd_colors[pos] = r as _;
                                cd_colors[pos + 1] = g as _;
                                cd_colors[pos + 2] = b as _;
                                cd_colors[pos + 3] = a as _;
                            }
                        }
                        cd.colors = cd_colors.into();
                        res = Some(cd);
                    }
                    if !img.is_null() {
                        XFree(img as _);
                    }
                }
            }
        }
    });
    match res {
        Some(x) => Ok(x),
        _ => bail!("Failed to get cursor image of {}", hcursor),
    }
}

fn start_uinput_service() {
    use crate::server::uinput::service;
    std::thread::spawn(|| {
        service::start_service_control();
    });
    std::thread::spawn(|| {
        service::start_service_keyboard();
    });
    std::thread::spawn(|| {
        service::start_service_mouse();
    });
}

/// Suggests the best terminal type based on the environment.
///
/// The function prioritizes terminal types in the following order:
/// 1. `screen-256color`: Preferred when running inside `tmux` or `screen` sessions,
///    as these multiplexers often support advanced terminal features.
/// 2. `xterm-256color`: Selected if the terminal supports 256 colors, which is
///    suitable for modern terminal applications.
/// 3. `xterm`: Used as a fallback for basic terminal compatibility.
///
/// Terminals like `linux` and `vt100` are excluded because they lack support for
/// modern features required by many applications.
fn suggest_best_term() -> String {
    if is_running_in_tmux() || is_running_in_screen() {
        return TERM_SCREEN_256COLOR.to_string();
    }
    if term_supports_256_colors(TERM_XTERM_256COLOR) {
        return TERM_XTERM_256COLOR.to_string();
    }
    TERM_XTERM.to_string()
}

fn is_running_in_tmux() -> bool {
    std::env::var("TMUX").is_ok()
}

fn is_running_in_screen() -> bool {
    std::env::var("STY").is_ok()
}

fn supports_256_colors(db: &Database) -> bool {
    db.get::<cap::MaxColors>().map_or(false, |n| n.0 >= 256)
}

fn term_supports_256_colors(term: &str) -> bool {
    match term {
        TERM_XTERM_256COLOR => DATABASE_XTERM_256COLOR
            .as_ref()
            .map_or(false, |db| supports_256_colors(db)),
        _ => Database::from_name(term).map_or(false, |db| supports_256_colors(&db)),
    }
}

fn get_cur_term(uid: &str) -> Option<String> {
    // Check cache first - if TERM_XTERM_256COLOR was found before, reuse it
    if let Ok(cache) = CACHED_TERM.lock() {
        if let Some(ref cached) = *cache {
            if cached == TERM_XTERM_256COLOR {
                return Some(cached.clone());
            }
        }
    }

    if uid.is_empty() {
        return None;
    }

    // Check current process environment
    if let Ok(term) = std::env::var("TERM") {
        if term == TERM_XTERM_256COLOR {
            if let Ok(mut cache) = CACHED_TERM.lock() {
                *cache = Some(term.clone());
            }
            return Some(term);
        }
    }

    // Collect all TERM values from shell processes, looking for TERM_XTERM_256COLOR
    let terms = get_all_term_values(uid);

    // Prefer TERM_XTERM_256COLOR
    if terms.iter().any(|t| t == TERM_XTERM_256COLOR) {
        if let Ok(mut cache) = CACHED_TERM.lock() {
            *cache = Some(TERM_XTERM_256COLOR.to_string());
        }
        return Some(TERM_XTERM_256COLOR.to_string());
    }

    // Return first valid TERM if no TERM_XTERM_256COLOR found
    let fallback = terms.into_iter().next();
    if let Some(ref term) = fallback {
        log::debug!(
            "TERM_XTERM_256COLOR not found, using fallback TERM: {}",
            term
        );
    }
    fallback
}

/// Get all TERM values from shell processes (bash, zsh, fish, sh).
/// Returns a Vec of unique, valid TERM values.
fn get_all_term_values(uid: &str) -> Vec<String> {
    let Ok(uid_num) = uid.parse::<u32>() else {
        return Vec::new();
    };

    // Build regex pattern to match shell processes using only argv[0] (the executable path)
    // Pattern: match process name at start or after '/', followed by space or end
    // e.g., "bash", "/bin/bash", "/usr/bin/zsh"
    let shell_pattern = SHELL_PROCESSES
        .iter()
        .map(|p| format!(r"(^|/){p}(\s|$)"))
        .collect::<Vec<_>>()
        .join("|");
    let Ok(re) = Regex::new(&shell_pattern) else {
        return Vec::new();
    };

    let Ok(entries) = std::fs::read_dir("/proc") else {
        return Vec::new();
    };

    let mut terms = Vec::new();

    for entry in entries.flatten() {
        let file_name = entry.file_name();
        let Some(pid_str) = file_name.to_str() else {
            continue;
        };
        if !pid_str.chars().all(|c| c.is_ascii_digit()) {
            continue;
        }

        let proc_path = entry.path();

        // Check if process belongs to the specified uid
        if let Ok(meta) = std::fs::metadata(&proc_path) {
            use std::os::unix::fs::MetadataExt;
            if meta.uid() != uid_num {
                continue;
            }
        } else {
            continue;
        }

        // Check cmdline matches process pattern
        // /proc/<pid>/cmdline is a sequence of null-terminated strings; the first
        // one (argv[0]) is the executable path. Match the regex only against that
        // to avoid false positives from arguments (e.g., "python /path/to/bash-script.py").
        let cmdline_path = proc_path.join("cmdline");
        let Ok(cmdline) = std::fs::read(&cmdline_path) else {
            continue;
        };
        let exe_end = cmdline
            .iter()
            .position(|&b| b == 0)
            .unwrap_or(cmdline.len());
        let exe_str = String::from_utf8_lossy(&cmdline[..exe_end]);
        if !re.is_match(&exe_str) {
            continue;
        }

        // Read environ and extract TERM
        let environ_path = proc_path.join("environ");
        let Ok(environ) = std::fs::read(&environ_path) else {
            continue;
        };

        for part in environ.split(|&b| b == 0) {
            if part.is_empty() {
                continue;
            }
            if let Some(eq) = part.iter().position(|&b| b == b'=') {
                let key_bytes = &part[..eq];
                if key_bytes == b"TERM" {
                    let val_bytes = &part[eq + 1..];
                    let term = String::from_utf8_lossy(val_bytes).into_owned();
                    if !INVALID_TERM_VALUES.contains(&term.as_str()) && !terms.contains(&term) {
                        // Early return if we found the preferred term
                        if term == TERM_XTERM_256COLOR {
                            return vec![term];
                        }
                        terms.push(term);
                    }
                    break;
                }
            }
        }
    }

    terms
}

#[inline]
fn try_start_server_(desktop: Option<&Desktop>) -> ResultType<Option<Child>> {
    match desktop {
        Some(desktop) => {
            let mut envs = vec![];
            if !desktop.display.is_empty() {
                envs.push(("DISPLAY", desktop.display.clone()));
            }
            if !desktop.xauth.is_empty() {
                envs.push(("XAUTHORITY", desktop.xauth.clone()));
            }
            if !desktop.wl_display.is_empty() {
                envs.push(("WAYLAND_DISPLAY", desktop.wl_display.clone()));
            }
            if !desktop.home.is_empty() {
                envs.push(("HOME", desktop.home.clone()));
            }
            if !desktop.dbus.is_empty() {
                envs.push(("DBUS_SESSION_BUS_ADDRESS", desktop.dbus.clone()));
            }
            if let Ok(forced_display_server) =
                std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER")
            {
                if !forced_display_server.is_empty() {
                    envs.push((
                        "RUSTDESK_FORCED_DISPLAY_SERVER",
                        forced_display_server,
                    ));
                }
            }
            envs.push((
                "TERM",
                get_cur_term(&desktop.uid).unwrap_or_else(|| suggest_best_term()),
            ));
            run_as_user(
                vec!["--server"],
                Some((desktop.uid.clone(), desktop.username.clone())),
                envs,
            )
        }
        None => Ok(Some(crate::run_me(vec!["--server"])?)),
    }
}

#[inline]
fn start_server(desktop: Option<&Desktop>, server: &mut Option<Child>) {
    match try_start_server_(desktop) {
        Ok(ps) => *server = ps,
        Err(err) => {
            log::error!("Failed to start server: {}", err);
        }
    }
}

/// Whether a just-spawned `--server` is still running after a short grace period, taking ownership of
/// the corpse (clearing `server`) when it is not. `start_server` reports only whether the SPAWN
/// succeeded, which is not the same question: a child that execs and exits immediately still leaves
/// `Some(child)` behind.
///
/// A child that exits is detected as soon as it does; a healthy one costs the full grace, once per
/// start. A server that dies LATER than this is a different (transient) failure, and the restart
/// throttle in `should_start_server` already bounds that case.
#[cfg(feature = "drm")]
fn server_survived_grace(server: &mut Option<Child>) -> bool {
    const GRACE: Duration = Duration::from_millis(1000);
    const STEP_MS: u64 = 100;
    let Some(ps) = server.as_mut() else {
        return false; // spawn itself failed
    };
    let deadline = Instant::now() + GRACE;
    while Instant::now() < deadline {
        match ps.try_wait() {
            Ok(Some(status)) => {
                log::warn!("--server exited {status} within {GRACE:?} of starting");
                *server = None;
                return false;
            }
            Ok(None) => sleep_millis(STEP_MS),
            // We cannot tell; treat it as alive rather than tearing down a possibly healthy child.
            Err(err) => {
                log::error!("error waiting on the just-started --server: {err}");
                return true;
            }
        }
    }
    true
}

fn stop_server(server: &mut Option<Child>) {
    if let Some(mut ps) = server.take() {
        allow_err!(ps.kill());
        sleep_millis(30);
        match ps.try_wait() {
            Ok(Some(_status)) => {}
            Ok(None) => {
                let _res = ps.wait();
            }
            Err(e) => log::error!("error attempting to wait: {e}"),
        }
    }
}

fn set_x11_env(desktop: &Desktop) {
    log::info!("DISPLAY: {}", desktop.display);
    log::info!("XAUTHORITY: {}", desktop.xauth);
    if !desktop.display.is_empty() {
        std::env::set_var("DISPLAY", &desktop.display);
    }
    if !desktop.xauth.is_empty() {
        std::env::set_var("XAUTHORITY", &desktop.xauth);
    }
}

#[inline]
fn stop_rustdesk_servers() {
    let _ = run_cmds(&format!(
        r##"ps -ef | grep -E '{} +--server' | awk '{{print $2}}' | xargs -r kill -9"##,
        crate::get_app_name().to_lowercase(),
    ));
}

fn should_start_server(
    try_x11: bool,
    is_display_changed: bool,
    uid: &mut String,
    desktop: &Desktop,
    cm0: &mut bool,
    last_restart: &mut Instant,
    server: &mut Option<Child>,
) -> bool {
    let cm = get_cm();
    let mut start_new = false;
    let mut should_kill = false;

    if is_display_changed || desktop.uid != *uid && !desktop.uid.is_empty() {
        *uid = desktop.uid.clone();
        if try_x11 {
            set_x11_env(&desktop);
        }
        should_kill = true;
    }

    if !should_kill
        && !cm
        && ((*cm0 && last_restart.elapsed().as_secs() > 60)
            || last_restart.elapsed().as_secs() > 3600)
    {
        let terminal_session_count = crate::ipc::get_terminal_session_count().unwrap_or(0);
        if terminal_session_count > 0 {
            // There are terminal sessions, so we don't restart the server.
            // We also need to keep `cm0` unchanged, so that we can reach this branch the next time.
            return false;
        }
        // restart server if new connections all closed, or every one hour,
        // as a workaround to resolve "SpotUdp" (dns resolve)
        // and x server get displays failure issue
        should_kill = true;
        log::info!("restart server");
    }

    if should_kill {
        if let Some(ps) = server.as_mut() {
            allow_err!(ps.kill());
            sleep_millis(30);
            *last_restart = Instant::now();
        }
    }

    if let Some(ps) = server.as_mut() {
        match ps.try_wait() {
            Ok(Some(_)) => {
                *server = None;
                start_new = true;
            }
            _ => {}
        }
    } else {
        start_new = true;
    }
    *cm0 = cm;
    start_new
}

// to-do: stop_server(&mut user_server); may not stop child correctly
// stop_rustdesk_servers() is just a temp solution here.
fn force_stop_server() {
    stop_rustdesk_servers();
    sleep_millis(super::SERVICE_INTERVAL);
}

pub fn start_os_service() {
    check_if_stop_service();
    stop_rustdesk_servers();
    start_uinput_service();

    std::thread::spawn(|| {
        allow_err!(crate::ipc::start(crate::POSTFIX_SERVICE));
    });

    // DRM/KMS capture producer (opt-in `drm` feature): a dedicated thread + runtime that streams
    // scanout frames to the user `--server` over the `_drm` service-scoped channel. Runs here
    // because this process is the root service that already holds CAP_SYS_ADMIN for the in-process
    // (direct-mode) libdrmtap read.
    //
    // Builder, like every other thread this feature starts: `thread::spawn` PANICS if the thread
    // cannot be created (EAGAIN under a thread-count or memory limit), and here that panic would
    // unwind out of `start_os_service` -- taking down the root service itself, for a feature whose
    // failure should only cost DRM capture. Losing the producer leaves the consumer to fall back to
    // PipeWire/X11, which is the same path a host without the feature takes.
    #[cfg(feature = "drm")]
    if let Err(err) = std::thread::Builder::new()
        .name("drm-producer".into())
        .spawn(|| {
            crate::ipc::start_drm();
        })
    {
        log::warn!(
            "failed to spawn the drm capture producer thread: {err}; DRM capture is off for \
             this boot and the consumer falls back to PipeWire/X11"
        );
    }

    let running = Arc::new(AtomicBool::new(true));
    let r = running.clone();
    let (mut display, mut xauth): (String, String) = ("".to_owned(), "".to_owned());
    let mut desktop = Desktop::default();
    let mut sid = "".to_owned();
    let mut uid = "".to_owned();
    let mut server: Option<Child> = None;
    let mut user_server: Option<Child> = None;
    if let Err(err) = ctrlc::set_handler(move || {
        r.store(false, Ordering::SeqCst);
    }) {
        println!("Failed to set Ctrl-C handler: {}", err);
    }

    let mut cm0 = false;
    let mut last_restart = Instant::now();
    while running.load(Ordering::SeqCst) {
        desktop.refresh();
        update_active_user_lookup_cache(&desktop);

        // Duplicate logic here with should_start_server.
        if desktop.username == "root" || desktop.is_login_wayland() {
            // try kill subprocess "--server"
            stop_server(&mut user_server);
            // try start subprocess "--server"
            // No need to check is_display_changed here.
            if should_start_server(
                true,
                false,
                &mut uid,
                &desktop,
                &mut cm0,
                &mut last_restart,
                &mut server,
            ) {
                force_stop_server();
                // Run the login-screen --server as the active seat0 session user (the greeter
                // account) rather than root, so the DRM capture GPU/EGL convert never loads the
                // vendor GPU userspace in a privileged process. is_login_wayland() matches a GDM or
                // SDDM Wayland greeter (is_gdm_user covers both), and desktop.uid is that greeter's
                // uid, so this drops to whichever greeter owns seat0. A greeter is_gdm_user does not
                // recognize (e.g. LightDM) never reaches this branch -- it takes the unprivileged
                // else-branch below already. A genuine root graphical session (username=="root")
                // has no lower uid to drop to, so it stays root. The whole branch is gated on the drm
                // feature, so the drm-off build is upstream's single `start_server(None, ..)` line.
                #[cfg(not(feature = "drm"))]
                start_server(None, &mut server);
                #[cfg(feature = "drm")]
                if desktop.username != "root" && !desktop.uid.is_empty() {
                    start_server(Some(&desktop), &mut server);
                    // If dropping to the greeter uid did not produce a RUNNING server, fall back to a
                    // root --server so the login screen stays remotable instead of looping on a
                    // failing greeter spawn. This pays the GPU-in-root tradeoff only on that failure
                    // path, never in the normal greeter case. Liveness, not just spawn success: a
                    // greeter account that cannot actually run it (a nologin shell, a hardened home,
                    // no writable config dir) leaves a child that exits at once, and the loop above
                    // notices only that the child is gone and respawns it, forever, without ever
                    // reaching this fallback -- so the login screen becomes permanently un-remotable
                    // on a host where it used to work.
                    if !server_survived_grace(&mut server) {
                        log::warn!(
                            "greeter --server did not stay up; falling back to a root --server"
                        );
                        start_server(None, &mut server);
                    }
                } else {
                    start_server(None, &mut server);
                }
            }
        } else if desktop.username != "" {
            // try kill subprocess "--server"
            stop_server(&mut server);

            let is_display_changed = desktop.display != display || desktop.xauth != xauth;
            display = desktop.display.clone();
            xauth = desktop.xauth.clone();

            // try start subprocess "--server"
            if should_start_server(
                !desktop.is_wayland(),
                is_display_changed,
                &mut uid,
                &desktop,
                &mut cm0,
                &mut last_restart,
                &mut user_server,
            ) {
                force_stop_server();
                start_server(Some(&desktop), &mut user_server);
            }
        } else {
            force_stop_server();
            stop_server(&mut user_server);
            stop_server(&mut server);
        }

        let keeps_session = sid == desktop.sid;
        if keeps_session {
            // for fixing https://github.com/rustdesk/rustdesk/issues/3129 to avoid too much dbus calling,
            sleep_millis(500);
        } else {
            sleep_millis(super::SERVICE_INTERVAL);
        }
        sid = desktop.sid.clone();
    }

    if let Some(ps) = user_server.take().as_mut() {
        allow_err!(ps.kill());
    }
    if let Some(ps) = server.take().as_mut() {
        allow_err!(ps.kill());
    }
    log::info!("Exit");
}

#[inline]
/// Returns the cached active `(uid, username)` snapshot when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_user_id_name() -> (String, String) {
    if let Some(id_name) = get_active_user_id_name_from_cache() {
        return id_name;
    }
    let vec_id_name = get_values_of_seat0(&[1, 2]);
    (vec_id_name[0].clone(), vec_id_name[1].clone())
}

#[inline]
/// Returns the cached active uid when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_userid() -> String {
    if let Some((uid, _)) = get_active_user_id_name_from_cache() {
        return uid;
    }
    get_values_of_seat0(&[1])[0].clone()
}

#[inline]
/// Returns the active uid from a fresh seat0 lookup, bypassing the service-loop cache.
pub fn get_active_userid_fresh() -> String {
    // A Wayland greeter owns seat0 while it is up and the DRM backend serves it, so a uid gate that
    // cannot see it rejects the greeter's own `--server`. `Desktop::refresh` reads it the same way.
    #[cfg(feature = "drm")]
    return get_values_of_seat0_with_gdm_wayland(&[1])[0].clone();
    #[cfg(not(feature = "drm"))]
    get_values_of_seat0(&[1])[0].clone()
}

#[inline]
/// The cached active uid as a number, or `None` when the cache is empty. Unlike `get_active_userid`
/// this NEVER falls back to a blocking `loginctl` seat0 lookup, so it is safe to call on an async
/// runtime thread and on a hot path (e.g. per-frame re-auth): a cache miss returns `None` for the
/// caller to treat as "active session momentarily unknown" rather than stalling on a subprocess.
pub fn get_active_userid_cached() -> Option<u32> {
    get_active_user_id_name_from_cache().and_then(|(uid, _)| uid.parse::<u32>().ok())
}

fn get_cm() -> bool {
    // Runs twice a second in the service loop, so walk /proc rather than forking `ps aux`; that
    // fork is also what the `CMD_PS` audit-message workaround this replaces was for.
    let cm = format!(
        "{} --cm",
        std::env::current_exe()
            .unwrap_or_default()
            .to_string_lossy()
    );
    any_process(None, "cmdline", |cmdline| cmdline.contains(&cm))
}

pub fn is_login_wayland() -> bool {
    let files = ["/etc/gdm3/custom.conf", "/etc/gdm/custom.conf"];
    match (
        Regex::new(r"# *WaylandEnable *= *false"),
        Regex::new(r"WaylandEnable *= *true"),
    ) {
        (Ok(pat1), Ok(pat2)) => {
            for file in files {
                if let Ok(contents) = std::fs::read_to_string(file) {
                    return pat1.is_match(&contents) || pat2.is_match(&contents);
                }
            }
        }
        _ => {}
    }
    false
}

#[inline]
pub fn current_is_wayland() -> bool {
    return is_desktop_wayland() && unsafe { UNMODIFIED };
}

// to-do: test the other display manager
fn _get_display_manager() -> String {
    if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") {
        if let Some(x) = x.split("/").last() {
            return x.to_owned();
        }
    }
    "gdm3".to_owned()
}

#[inline]
/// Returns the cached active username when available.
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
pub fn get_active_username() -> String {
    if let Some((_, username)) = get_active_user_id_name_from_cache() {
        return username;
    }
    get_values_of_seat0(&[2])[0].clone()
}

pub fn get_user_home_by_name(username: &str) -> Option<PathBuf> {
    return match get_user_by_name(username) {
        None => None,
        Some(user) => {
            let home = user.home_dir();
            if Path::is_dir(home) {
                Some(PathBuf::from(home))
            } else {
                None
            }
        }
    };
}

pub fn get_active_user_home() -> Option<PathBuf> {
    let username = get_active_username();
    if !username.is_empty() {
        match get_user_home_by_name(&username) {
            None => {
                // fallback to most common default pattern
                let home = PathBuf::from(format!("/home/{}", username));
                if home.exists() {
                    return Some(home);
                }
            }
            Some(home) => {
                return Some(home);
            }
        }
    }
    None
}

pub fn get_env_var(k: &str) -> String {
    match std::env::var(k) {
        Ok(v) => v,
        Err(_e) => "".to_owned(),
    }
}

fn is_flatpak() -> bool {
    std::path::PathBuf::from("/.flatpak-info").exists()
}

pub fn is_prelogin() -> bool {
    if is_flatpak() {
        return false;
    }
    let name = get_active_username();
    if let Ok(res) = run_cmds(&format!("getent passwd {}", name)) {
        return res.contains("/bin/false") || res.contains("/usr/sbin/nologin");
    }
    false
}

// Check "Lock".
// "Switch user" can't be checked, because `get_values_of_seat0(&[0])` does not return the session.
// The logged in session is "online" not "active".
// And the "Switch user" screen is usually Wayland login session, which we do not support.
pub fn is_locked() -> bool {
    if is_prelogin() {
        return false;
    }

    let values = get_values_of_seat0(&[0]);
    // Though the values can't be empty, we still add check here for safety.
    // Because we cannot guarantee whether the internal implementation will change in the future.
    // https://github.com/rustdesk/hbb_common/blob/ebb4d4a48cf7ed6ca62e93f8ed124065c6408536/src/platform/linux.rs#L119
    if values.is_empty() {
        log::debug!("Failed to check is locked, values vector is empty.");
        return false;
    }
    let session = &values[0];
    if session.is_empty() {
        log::debug!("Failed to check is locked, session is empty.");
        return false;
    }
    is_session_locked(session)
}

pub fn is_root() -> bool {
    crate::username() == "root"
}

fn is_opensuse() -> bool {
    if let Ok(res) = run_cmds("cat /etc/os-release | grep opensuse") {
        if !res.is_empty() {
            return true;
        }
    }
    false
}

pub fn run_as_user<I, K, V>(
    arg: Vec<&str>,
    user: Option<(String, String)>,
    envs: I,
) -> ResultType<Option<Child>>
where
    I: IntoIterator<Item = (K, V)>,
    K: AsRef<OsStr>,
    V: AsRef<OsStr>,
{
    let (uid, username) = match user {
        Some(id_name) => id_name,
        None => get_active_user_id_name(),
    };
    let cmd = std::env::current_exe()?;
    if uid.is_empty() {
        bail!("No valid uid");
    }

    let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{uid}");
    if *SUDO_E_PRESERVES_ENV {
        // Original logic: use sudo -E to preserve environment
        let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or("")];
        args.append(&mut arg.clone());
        // -E is required to preserve env
        args.insert(0, "-E");
        let task = Command::new("sudo").envs(envs).args(args).spawn()?;
        Ok(Some(task))
    } else {
        // Fallback: sudo -u username env VAR=VALUE ... cmd args
        // For systems where sudo -E is not supported (e.g., Ubuntu 25.10+)
        //
        // SECURITY: No shell is involved here (we use execve-style argv).
        // Environment is passed via `env` arguments,
        // so there is no shell injection vector.
        //
        // Only accept portable env var names (POSIX portable character set for shells).
        // Most legitimate env vars follow [A-Za-z_][A-Za-z0-9_]* convention.
        // Variables with dots (e.g., "java.home") are Java system properties, not env vars.
        // Being restrictive here is intentional for security in this sudo context.
        fn is_valid_env_key(key: &str) -> bool {
            let mut it = key.chars();
            match it.next() {
                Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
                _ => return false,
            }
            it.all(|c| c.is_ascii_alphanumeric() || c == '_')
        }

        let mut sudo = Command::new("sudo");
        sudo.arg("-u").arg(&username).arg("--").arg("env").arg(xdg);

        for (k, v) in envs {
            let key = k.as_ref().to_string_lossy();
            if !is_valid_env_key(&key) {
                log::warn!("Skipping environment variable with invalid key: '{}'. Only [A-Za-z_][A-Za-z0-9_]* are allowed in sudo context.", key);
                continue;
            }
            // IMPORTANT: do NOT add shell quotes here; `Command` does not invoke a shell.
            // Passing KEY=VALUE as a single argv element is safe and preserves spaces.
            let mut arg = OsString::from(&*key);
            arg.push("=");
            arg.push(v.as_ref());
            sudo.arg(arg);
        }

        sudo.arg(cmd).args(arg);
        let task = sudo.spawn()?;
        Ok(Some(task))
    }
}

pub fn get_pa_monitor() -> String {
    get_pa_sources()
        .drain(..)
        .map(|x| x.0)
        .filter(|x| x.contains("monitor"))
        .next()
        .unwrap_or("".to_owned())
}

pub fn get_pa_source_name(desc: &str) -> String {
    get_pa_sources()
        .drain(..)
        .filter(|x| x.1 == desc)
        .map(|x| x.0)
        .next()
        .unwrap_or("".to_owned())
}

pub fn get_pa_sources() -> Vec<(String, String)> {
    use pulsectl::controllers::*;
    let mut out = Vec::new();
    match SourceController::create() {
        Ok(mut handler) => {
            if let Ok(devices) = handler.list_devices() {
                for dev in devices.clone() {
                    out.push((
                        dev.name.unwrap_or("".to_owned()),
                        dev.description.unwrap_or("".to_owned()),
                    ));
                }
            }
        }
        Err(err) => {
            log::error!("Failed to get_pa_sources: {:?}", err);
        }
    }
    out
}

pub fn get_default_pa_source() -> Option<(String, String)> {
    use pulsectl::controllers::*;
    match SourceController::create() {
        Ok(mut handler) => {
            if let Ok(dev) = handler.get_default_device() {
                return Some((
                    dev.name.unwrap_or("".to_owned()),
                    dev.description.unwrap_or("".to_owned()),
                ));
            }
        }
        Err(err) => {
            log::error!("Failed to get_pa_source: {:?}", err);
        }
    }
    None
}

pub fn lock_screen() {
    Command::new("xdg-screensaver").arg("lock").spawn().ok();
}

pub fn toggle_blank_screen(_v: bool) {
    // https://unix.stackexchange.com/questions/17170/disable-keyboard-mouse-input-on-unix-under-x
}

pub fn block_input(_v: bool) -> (bool, String) {
    (true, "".to_owned())
}

pub fn is_installed() -> bool {
    if let Ok(p) = std::env::current_exe() {
        p.to_str().unwrap_or_default().starts_with("/usr")
            || p.to_str().unwrap_or_default().starts_with("/nix/store")
    } else {
        false
    }
}

/// Get multiple environment variables from a process matching the given criteria.
/// This version reads /proc directly instead of spawning shell commands.
///
/// # Arguments
/// * `uid` - User ID to filter processes
/// * `process_pat` - Regex pattern to match process cmdline
/// * `names` - Environment variable names to retrieve. **Must be <= 64 elements** due to
///   the internal bitmask used for tie-breaking.
///
/// # Panics (debug builds)
/// Panics if `names.len() > 64`.
///
/// # Implementation notes
/// - Returns values from a *single* best-matching process_pat (for consistency).
/// - Avoids repeated scanning by parsing `environ` once per process.
fn get_envs<'a>(
    uid: &str,
    process_pat: &str,
    names: &[&'a str],
) -> std::collections::HashMap<&'a str, String> {
    get_envs_where(uid, process_pat, names, false, |count| count == names.len())
}

/// The newest process matching `process_pat`, whatever it happens to carry: the semantics of the
/// `ps -u <uid> -f | grep <pat> | tail -1` pipeline the callers below used before. A variable this
/// process does not have means moving on to the next pattern, never on to an older process that
/// may belong to a session which has since logged out.
fn get_envs_of_newest<'a>(
    uid: &str,
    process_pat: &str,
    names: &[&'a str],
) -> std::collections::HashMap<&'a str, String> {
    get_envs_where(uid, process_pat, names, true, |_| true)
}

/// `get_envs` with the caller's own process order and its own notion of a complete answer, told
/// how many of `names` the process carries: the first process `accept` takes wins outright, and
/// the count-based ranking is only the fallback for when no process is accepted at all.
fn get_envs_where<'a, F>(
    uid: &str,
    process_pat: &str,
    names: &[&'a str],
    newest_first: bool,
    mut accept: F,
) -> std::collections::HashMap<&'a str, String>
where
    F: FnMut(usize) -> bool,
{
    // The tie-breaking logic uses a u64 bitmask, limiting us to 64 variables.
    debug_assert!(
        names.len() <= 64,
        "get_envs: names.len() must be <= 64, got {}",
        names.len()
    );

    let empty: std::collections::HashMap<&'a str, String> =
        names.iter().map(|&n| (n, String::new())).collect();

    let Ok(uid_num) = uid.parse::<u32>() else {
        return empty;
    };
    let Ok(re) = Regex::new(process_pat) else {
        return empty;
    };

    // Used for stable tie-breaking when multiple processes match.
    // Higher bits correspond to earlier entries in `names`.
    let name_indices: std::collections::HashMap<&'a str, usize> =
        names.iter().enumerate().map(|(i, &n)| (n, i)).collect();

    let mut best = empty.clone();
    let mut best_count = 0usize;
    let mut best_mask: u64 = 0;

    // Iterate /proc to find matching processes. `newest_first` is only for `get_envs_of_newest`,
    // whose callers need the last PID-ordered match their `ps ... | tail -1` pipelines took;
    // without it the order is whatever readdir returns, which is what `get_envs` has always used.
    // Neither order identifies the active session -- a user with two live graphical sessions has
    // one of each, and picking by PID guesses. See `Desktop::refresh` for who owns that question.
    let Ok(entries) = std::fs::read_dir("/proc") else {
        return best;
    };
    let mut pids: Vec<u32> = entries
        .flatten()
        .filter_map(|entry| entry.file_name().to_str()?.parse::<u32>().ok())
        .collect();
    if newest_first {
        pids.sort_unstable_by(|a, b| b.cmp(a));
    }

    for pid in pids {
        let proc_path = std::path::Path::new("/proc").join(pid.to_string());

        // Check if process belongs to the specified uid
        if let Ok(meta) = std::fs::metadata(&proc_path) {
            use std::os::unix::fs::MetadataExt;
            if meta.uid() != uid_num {
                continue;
            }
        } else {
            continue;
        }

        // Check cmdline matches process pattern
        let cmdline_path = proc_path.join("cmdline");
        let Ok(cmdline) = std::fs::read(&cmdline_path) else {
            continue;
        };
        let cmdline_str = String::from_utf8_lossy(&cmdline).replace('\0', " ");
        // The `grep -v 'grep'` of the pipeline this replaces. A user grepping for one of these
        // patterns is otherwise the newest match for it, and answers with whatever environment
        // their shell had -- an X forwarding endpoint over ssh, say.
        if cmdline_str.contains("grep") || !re.is_match(&cmdline_str) {
            continue;
        }

        // Read environ and extract matching variables. A read that fails -- the process exited
        // between these two reads -- is a process carrying none of `names`, not a process to
        // skip: skipping it would hand `newest_first` on to an older PID, where the pipeline
        // this replaces stopped at the single PID its `tail -1` had already picked.
        let environ = std::fs::read(proc_path.join("environ")).unwrap_or_default();

        let mut found = empty.clone();
        let mut found_count = 0usize;
        let mut found_mask: u64 = 0;

        for part in environ.split(|&b| b == 0) {
            if part.is_empty() {
                continue;
            }
            let Some(eq) = part.iter().position(|&b| b == b'=') else {
                continue;
            };
            let key_bytes = &part[..eq];
            let val_bytes = &part[eq + 1..];

            let Ok(key) = std::str::from_utf8(key_bytes) else {
                continue;
            };
            if let Some(slot) = found.get_mut(key) {
                if slot.is_empty() {
                    *slot = String::from_utf8_lossy(val_bytes).into_owned();
                    found_count += 1;

                    if let Some(&idx) = name_indices.get(key) {
                        let total = names.len();
                        if total <= 64 {
                            let bit = 1u64 << (total - 1 - idx);
                            found_mask |= bit;
                        }
                    }
                }
            }
        }

        if accept(found_count) {
            return found;
        }

        if found_count > best_count || (found_count == best_count && found_mask > best_mask) {
            best = found;
            best_count = found_count;
            best_mask = found_mask;
        }
    }

    best
}

/// True when `pred` accepts the `/proc/<pid>/<file>` of any process, NULs turned into spaces,
/// optionally only of processes owned by `uid`.
/// Reads `/proc` directly instead of forking `ps` / `pgrep`, for the service-loop callers below.
fn any_process<F: Fn(&str) -> bool>(uid: Option<u32>, file: &str, pred: F) -> bool {
    let Ok(entries) = std::fs::read_dir("/proc") else {
        return false;
    };
    for entry in entries.flatten() {
        let file_name = entry.file_name();
        let Some(pid_str) = file_name.to_str() else {
            continue;
        };
        if !pid_str.chars().all(|c| c.is_ascii_digit()) {
            continue;
        }
        let proc_path = entry.path();
        if let Some(uid) = uid {
            use std::os::unix::fs::MetadataExt;
            match std::fs::metadata(&proc_path) {
                Ok(meta) if meta.uid() == uid => {}
                _ => continue,
            }
        }
        let Ok(content) = std::fs::read(proc_path.join(file)) else {
            continue;
        };
        if pred(&String::from_utf8_lossy(&content).replace('\0', " ")) {
            return true;
        }
    }
    false
}

#[inline]
fn get_env_from_pid(name: &str, pid: &str) -> String {
    let cmd = format!("cat /proc/{}/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", pid, name, name);
    if let Ok(x) = run_cmds(&cmd) {
        x.trim_end().to_string()
    } else {
        "".to_owned()
    }
}

#[link(name = "gtk-3")]
extern "C" {
    fn gtk_main_quit();
}

pub fn quit_gui() {
    unsafe { gtk_main_quit() };
}

/*
pub fn exec_privileged(args: &[&str]) -> ResultType<Child> {
    Ok(Command::new("pkexec").args(args).spawn()?)
}
*/

pub fn check_super_user_permission() -> ResultType<bool> {
    gtk_sudo::run(vec!["echo"])?;
    Ok(true)
}

/*
pub fn elevate(args: Vec<&str>) -> ResultType<bool> {
    let cmd = std::env::current_exe()?;
    match cmd.to_str() {
        Some(cmd) => {
            let mut args_with_exe = vec![cmd];
            args_with_exe.append(&mut args.clone());
            // -E required for opensuse
            if is_opensuse() {
                args_with_exe.insert(0, "-E");
            }
            let res = match exec_privileged(&args_with_exe)?.wait() {
                Ok(status) => {
                    if status.success() {
                        true
                    } else {
                        log::error!(
                            "Failed to wait install process, process status: {:?}",
                            status
                        );
                        false
                    }
                }
                Err(e) => {
                    log::error!("Failed to wait install process, error: {}", e);
                    false
                }
            };
            Ok(res)
        }
        None => {
            hbb_common::bail!("Failed to get current exe as str");
        }
    }
}
*/

type GtkSettingsPtr = *mut c_void;
type GObjectPtr = *mut c_void;
#[link(name = "gtk-3")]
extern "C" {
    // fn gtk_init(argc: *mut c_int, argv: *mut *mut c_char);
    fn gtk_settings_get_default() -> GtkSettingsPtr;
}

#[link(name = "gobject-2.0")]
extern "C" {
    fn g_object_get(object: GObjectPtr, first_property_name: *const c_char, ...);
}

pub fn get_double_click_time() -> u32 {
    // GtkSettings *settings = gtk_settings_get_default ();
    // g_object_get (settings, "gtk-double-click-time", &double_click_time, NULL);
    unsafe {
        let mut double_click_time = 0u32;
        let Ok(property) = std::ffi::CString::new("gtk-double-click-time") else {
            return 0;
        };
        let settings = gtk_settings_get_default();
        g_object_get(
            settings,
            property.as_ptr(),
            &mut double_click_time as *mut u32,
            0 as *const c_void,
        );
        double_click_time
    }
}

#[inline]
fn get_width_height_from_captures<'t>(caps: &Captures<'t>) -> Option<(i32, i32)> {
    match (caps.name("width"), caps.name("height")) {
        (Some(width), Some(height)) => {
            match (
                width.as_str().parse::<i32>(),
                height.as_str().parse::<i32>(),
            ) {
                (Ok(width), Ok(height)) => {
                    return Some((width, height));
                }
                _ => {}
            }
        }
        _ => {}
    }
    None
}

#[inline]
fn get_xrandr_conn_pat(name: &str) -> String {
    format!(
        r"{}\s+connected.+?(?P<width>\d+)x(?P<height>\d+)\+(?P<x>\d+)\+(?P<y>\d+).*?\n",
        name
    )
}

pub fn resolutions(name: &str) -> Vec<Resolution> {
    let resolutions_pat = r"(?P<resolutions>(\s*\d+x\d+\s+\d+.*\n)+)";
    let connected_pat = get_xrandr_conn_pat(name);
    let mut v = vec![];
    if let Ok(re) = Regex::new(&format!("{}{}", connected_pat, resolutions_pat)) {
        match run_cmds("xrandr --query | tr -s ' '") {
            Ok(xrandr_output) => {
                // There'are different kinds of xrandr output.
                /*
                1.
                Screen 0: minimum 320 x 175, current 1920 x 1080, maximum 1920 x 1080
                default connected 1920x1080+0+0 0mm x 0mm
                 1920x1080 10.00*
                 1280x720 25.00
                 1680x1050 60.00
                Virtual2 disconnected (normal left inverted right x axis y axis)
                Virtual3 disconnected (normal left inverted right x axis y axis)

                Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384
                eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 344mm x 193mm
                1920x1080     60.01*+  60.01    59.97    59.96    59.93
                1680x1050     59.95    59.88
                1600x1024     60.17

                XWAYLAND0 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm
                Virtual1 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm
                HDMI-0 connected (normal left inverted right x axis y axis)

                rdp0 connected primary 1920x1080+0+0 0mm x 0mm
                    */
                if let Some(caps) = re.captures(&xrandr_output) {
                    if let Some(resolutions) = caps.name("resolutions") {
                        let resolution_pat =
                            r"\s*(?P<width>\d+)x(?P<height>\d+)\s+(?P<rates>(\d+\.\d+\D*)+)\s*\n";
                        let Ok(resolution_re) = Regex::new(&format!(r"{}", resolution_pat)) else {
                            log::error!("Regex new failed");
                            return vec![];
                        };
                        for resolution_caps in resolution_re.captures_iter(resolutions.as_str()) {
                            if let Some((width, height)) =
                                get_width_height_from_captures(&resolution_caps)
                            {
                                let resolution = Resolution {
                                    width,
                                    height,
                                    ..Default::default()
                                };
                                if !v.contains(&resolution) {
                                    v.push(resolution);
                                }
                            }
                        }
                    }
                }
            }
            Err(e) => log::error!("Failed to run xrandr query, {}", e),
        }
    }

    v
}

pub fn current_resolution(name: &str) -> ResultType<Resolution> {
    let xrandr_output = run_cmds("xrandr --query | tr -s ' '")?;
    let re = Regex::new(&get_xrandr_conn_pat(name))?;
    if let Some(caps) = re.captures(&xrandr_output) {
        if let Some((width, height)) = get_width_height_from_captures(&caps) {
            return Ok(Resolution {
                width,
                height,
                ..Default::default()
            });
        }
    }
    bail!("Failed to find current resolution for {}", name);
}

pub fn change_resolution_directly(name: &str, width: usize, height: usize) -> ResultType<()> {
    Command::new("xrandr")
        .args(vec![
            "--output",
            name,
            "--mode",
            &format!("{}x{}", width, height),
        ])
        .spawn()?;
    Ok(())
}

/// Scoped to `uid`, the user of the session being refreshed: the compositor starts Xwayland as
/// that user, so another user's Xwayland -- a switched-away session, a second seat -- answering
/// this used to route a pure-Wayland session into the Xwayland probe, which has no display for
/// it to find. A uid that cannot be parsed falls back to the unscoped answer.
#[inline]
pub fn is_xwayland_running(uid: &str) -> bool {
    // Same test as the `pgrep -a Xwayland` this replaces: the process name, not its command line.
    any_process(uid.parse::<u32>().ok(), "comm", |comm| {
        comm.contains("Xwayland")
    })
}

mod desktop {
    use super::*;

    pub const XFCE4_PANEL: &str = "xfce4-panel";
    pub const SDDM_GREETER: &str = "sddm-greeter";

    // xdg-desktop-portal runs on all Wayland desktops (GNOME, KDE, wlroots, etc.)
    const XDG_DESKTOP_PORTAL: &str = "xdg-desktop-portal";
    const XWAYLAND: &str = "Xwayland";
    const IBUS_DAEMON: &str = "ibus-daemon";
    const PLASMA_KDED: &str = "kded[0-9]+";
    const GNOME_GOA_DAEMON: &str = "goa-daemon";

    const ENV_KEY_DISPLAY: &str = "DISPLAY";
    const ENV_KEY_XAUTHORITY: &str = "XAUTHORITY";
    const ENV_KEY_WAYLAND_DISPLAY: &str = "WAYLAND_DISPLAY";
    const ENV_KEY_DBUS_SESSION_BUS_ADDRESS: &str = "DBUS_SESSION_BUS_ADDRESS";

    /// A compositor that runs Xwayland without exporting `XAUTHORITY` (wlroots, e.g. Hyprland)
    /// still hands out a usable session through the Wayland side. Requiring xauth there never
    /// succeeded, so every refresh ran the retry loop to the end.
    /// https://github.com/rustdesk/rustdesk/issues/15952
    fn is_session_env_complete(envs: &std::collections::HashMap<&str, String>) -> bool {
        let value = |key: &str| envs.get(key).map_or("", |v| v.as_str());
        !value(ENV_KEY_DISPLAY).is_empty()
            && (!value(ENV_KEY_XAUTHORITY).is_empty()
                || (!value(ENV_KEY_WAYLAND_DISPLAY).is_empty()
                    && !value(ENV_KEY_DBUS_SESSION_BUS_ADDRESS).is_empty()))
    }

    #[derive(Debug, Clone, Default)]
    pub struct Desktop {
        pub sid: String,
        pub username: String,
        pub uid: String,
        pub protocol: String,
        pub display: String,
        pub xauth: String,
        pub home: String,
        pub dbus: String,
        pub wl_display: String,
    }

    impl Desktop {
        #[inline]
        pub fn is_wayland(&self) -> bool {
            self.protocol == DISPLAY_SERVER_WAYLAND
        }

        #[inline]
        pub fn is_login_wayland(&self) -> bool {
            super::is_gdm_user(&self.username) && self.protocol == DISPLAY_SERVER_WAYLAND
        }

        fn get_display_xauth_wayland(&mut self) {
            for _ in 1..=10 {
                // Prefer Wayland-related variables first when multiple portal processes match.
                let mut envs = get_envs(
                    &self.uid,
                    XDG_DESKTOP_PORTAL,
                    &[
                        ENV_KEY_WAYLAND_DISPLAY,
                        ENV_KEY_DBUS_SESSION_BUS_ADDRESS,
                        ENV_KEY_DISPLAY,
                        ENV_KEY_XAUTHORITY,
                    ],
                );
                self.display = envs.remove(ENV_KEY_DISPLAY).unwrap_or_default();
                self.xauth = envs.remove(ENV_KEY_XAUTHORITY).unwrap_or_default();
                self.wl_display = envs.remove(ENV_KEY_WAYLAND_DISPLAY).unwrap_or_default();
                self.dbus = envs
                    .remove(ENV_KEY_DBUS_SESSION_BUS_ADDRESS)
                    .unwrap_or_default();
                // For pure Wayland sessions, prefer `WAYLAND_DISPLAY`.
                // NOTE: On some systems (e.g. Ubuntu 25.10), `DISPLAY`/`XAUTHORITY` may exist even when XWayland
                // is not running, so do NOT treat them as a success condition here.
                let has_wayland = !self.wl_display.is_empty();
                let has_dbus = !self.dbus.is_empty();
                if has_wayland && has_dbus {
                    return;
                }
                sleep_millis(300);
            }
        }

        fn get_display_xauth_xwayland(&mut self) {
            let tray = format!("{} +--tray", crate::get_app_name().to_lowercase());
            for _ in 1..=10 {
                let display_proc = vec![
                    XDG_DESKTOP_PORTAL,
                    XWAYLAND,
                    IBUS_DAEMON,
                    GNOME_GOA_DAEMON,
                    PLASMA_KDED,
                    tray.as_str(),
                ];
                self.display.clear();
                self.xauth.clear();
                self.wl_display.clear();
                self.dbus.clear();
                let mut kept = 0u8;
                for proc in display_proc {
                    let mut envs = get_envs_of_newest(
                        &self.uid,
                        proc,
                        &[
                            ENV_KEY_DISPLAY,
                            ENV_KEY_XAUTHORITY,
                            ENV_KEY_WAYLAND_DISPLAY,
                            ENV_KEY_DBUS_SESSION_BUS_ADDRESS,
                        ],
                    );
                    let complete = is_session_env_complete(&envs);
                    let display = envs.remove(ENV_KEY_DISPLAY).unwrap_or_default();
                    let xauth = envs.remove(ENV_KEY_XAUTHORITY).unwrap_or_default();
                    let wl_display = envs.remove(ENV_KEY_WAYLAND_DISPLAY).unwrap_or_default();
                    let dbus = envs
                        .remove(ENV_KEY_DBUS_SESSION_BUS_ADDRESS)
                        .unwrap_or_default();
                    // Take a candidate whole. Two graphical sessions of one user each answer
                    // some of these, and a display paired with another session's xauth or
                    // compositor is a pair that never existed. So rank candidates rather than
                    // merge them, and keep the best seen: the later patterns are fallbacks.
                    //
                    // The Wayland-only rank matters when `is_xwayland_running` matched some other
                    // user's Xwayland and this session has none of its own. Nothing here can then
                    // answer with a display, and dropping the candidate for that would leave the
                    // child server without the compositor and bus of a session that is perfectly
                    // serveable through them.
                    let rank = if complete {
                        3
                    } else if !wl_display.is_empty() && !dbus.is_empty() {
                        2
                    } else if !display.is_empty() {
                        1
                    } else {
                        0
                    };
                    if rank > kept {
                        kept = rank;
                        self.display = display;
                        self.xauth = xauth;
                        self.wl_display = wl_display;
                        self.dbus = dbus;
                    }
                    if complete {
                        return;
                    }
                }
                // The Wayland pair on its own is a session the child server can be started
                // against -- it is what `get_display_xauth_wayland` returns on. Retrying is for a
                // session that has not finished coming up, and a compositor whose Xwayland starts
                // on demand may never export a `DISPLAY` for this walk to find, so waiting ten
                // more rounds for one costs the whole probe again on every refresh.
                if kept >= 2 {
                    break;
                }
                sleep_millis(300);
            }
        }

        fn get_display_x11(&mut self) {
            for _ in 1..=10 {
                let display_proc = vec![
                    XWAYLAND,
                    IBUS_DAEMON,
                    GNOME_GOA_DAEMON,
                    PLASMA_KDED,
                    XFCE4_PANEL,
                    SDDM_GREETER,
                ];
                for proc in display_proc {
                    self.display = get_envs_of_newest(&self.uid, proc, &[ENV_KEY_DISPLAY])
                        .remove(ENV_KEY_DISPLAY)
                        .unwrap_or_default();
                    if !self.display.is_empty() {
                        break;
                    }
                }
                if !self.display.is_empty() {
                    break;
                }
                sleep_millis(300);
            }

            if self.display.is_empty() {
                // logind stores the value pam_systemd was handed at session creation, which is not
                // necessarily a local display: it can be qualified with this host (`myhost:0`) or
                // name an X forwarding endpoint (`localhost:10.0`), and some setups record a bare
                // `:`. Strip this host, then require a display number. `localhost` is deliberately
                // left in place: a non-empty display here suppresses every fallback below, both
                // `get_display_by_user` and the `:0` default, so anything not local must not pass.
                let display = Self::get_display_from_session(&self.sid)
                    .replace(&hbb_common::whoami::hostname(), "");
                if display.strip_prefix(':').map_or(false, |number| {
                    number.starts_with(|c: char| c.is_ascii_digit())
                }) {
                    self.display = display;
                }
            }
            if self.display.is_empty() {
                self.display = Self::get_display_by_user(&self.username);
            }
            if self.display.is_empty() {
                self.display = ":0".to_owned();
            }
            self.display = self
                .display
                .replace(&hbb_common::whoami::hostname(), "")
                .replace("localhost", "");
        }

        fn get_display_from_session(session: &str) -> String {
            if session.is_empty() {
                return String::new();
            }

            match Command::new(CMD_LOGINCTL.as_str())
                .args(["show-session", "-p", "Display", session])
                .output()
            {
                Ok(output) if output.status.success() => String::from_utf8_lossy(&output.stdout)
                    .trim()
                    .strip_prefix("Display=")
                    .unwrap_or_default()
                    .to_owned(),
                Ok(output) => {
                    log::debug!(
                        "Failed to get display for session {session}: {}",
                        output.status
                    );
                    String::new()
                }
                Err(err) => {
                    log::debug!("Failed to get display for session {session}: {err}");
                    String::new()
                }
            }
        }

        fn get_home(&mut self) {
            self.home = "".to_string();

            let cmd = format!(
                "getent passwd '{}' | awk -F':' '{{print $6}}'",
                &self.username
            );
            self.home = run_cmds_trim_newline(&cmd).unwrap_or(format!("/home/{}", &self.username));
        }

        fn get_xauth_from_xorg(&mut self) {
            if let Ok(output) = run_cmds(&format!(
                "ps -u {} -f | grep 'Xorg' | grep -v 'grep'",
                &self.uid
            )) {
                for line in output.lines() {
                    let mut auth_found = false;

                    for v in line.split_whitespace() {
                        if v == "-auth" {
                            auth_found = true;
                        } else if auth_found {
                            if std::path::Path::new(v).is_absolute()
                                && std::path::Path::new(v).exists()
                            {
                                self.xauth = v.to_string();
                            } else {
                                if let Some(pid) = line.split_whitespace().nth(1) {
                                    let mut base_dir: String = String::from("/home"); // default pattern
                                    let home_dir = get_env_from_pid("HOME", pid);
                                    if home_dir.is_empty() {
                                        if let Some(home) = get_user_home_by_name(&self.username) {
                                            base_dir = home.as_path().to_string_lossy().to_string();
                                        };
                                    } else {
                                        base_dir = home_dir;
                                    }
                                    if Path::new(&base_dir).exists() {
                                        self.xauth = format!("{}/{}", base_dir, v);
                                    };
                                } else {
                                    // unreachable!
                                }
                            }
                            return;
                        }
                    }
                }
            }
        }

        fn get_xauth_x11(&mut self) {
            // try by direct access to window manager process by name
            let tray = format!("{} +--tray", crate::get_app_name().to_lowercase());
            for _ in 1..=10 {
                let display_proc = vec![
                    XWAYLAND,
                    IBUS_DAEMON,
                    GNOME_GOA_DAEMON,
                    PLASMA_KDED,
                    XFCE4_PANEL,
                    SDDM_GREETER,
                    tray.as_str(),
                ];
                for proc in display_proc {
                    self.xauth = get_envs_of_newest(&self.uid, proc, &[ENV_KEY_XAUTHORITY])
                        .remove(ENV_KEY_XAUTHORITY)
                        .unwrap_or_default();
                    if !self.xauth.is_empty() {
                        break;
                    }
                }
                if !self.xauth.is_empty() {
                    break;
                }
                sleep_millis(300);
            }

            // get from Xorg process, parameter and environment
            if self.xauth.is_empty() {
                self.get_xauth_from_xorg();
            }

            // fallback to default file name
            if self.xauth.is_empty() {
                let gdm = format!("/run/user/{}/gdm/Xauthority", self.uid);
                self.xauth = if std::path::Path::new(&gdm).exists() {
                    gdm
                } else {
                    let username = &self.username;
                    match get_user_home_by_name(username) {
                        None => {
                            if username == "root" {
                                format!("/{}/.Xauthority", username)
                            } else {
                                let tmp = format!("/home/{}/.Xauthority", username);
                                if std::path::Path::new(&tmp).exists() {
                                    tmp
                                } else {
                                    format!("/var/lib/{}/.Xauthority", username)
                                }
                            }
                        }
                        Some(home) => {
                            format!(
                                "{}/.Xauthority",
                                home.as_path().to_string_lossy().to_string()
                            )
                        }
                    }
                };
            }
        }

        fn get_display_by_user(user: &str) -> String {
            // log::debug!("w {}", &user);
            if let Ok(output) = std::process::Command::new("w").arg(&user).output() {
                for line in String::from_utf8_lossy(&output.stdout).lines() {
                    let mut iter = line.split_whitespace();
                    let b = iter.nth(2);
                    if let Some(b) = b {
                        if b.starts_with(":") {
                            return b.to_owned();
                        }
                    }
                }
            }
            // above not work for gdm user
            //log::debug!("ls -l /tmp/.X11-unix/");
            let mut last = "".to_owned();
            if let Ok(output) = std::process::Command::new("ls")
                .args(vec!["-l", "/tmp/.X11-unix/"])
                .output()
            {
                for line in String::from_utf8_lossy(&output.stdout).lines() {
                    let mut iter = line.split_whitespace();
                    let user_field = iter.nth(2);
                    if let Some(x) = iter.last() {
                        if x.starts_with("X") {
                            last = x.replace("X", ":").to_owned();
                            if user_field == Some(&user) {
                                return last;
                            }
                        }
                    }
                }
            }
            last
        }

        pub fn refresh(&mut self) {
            if !self.sid.is_empty() && is_active_and_seat0(&self.sid) {
                // Xwayland display and xauth may not be available in a short time after login.
                if is_xwayland_running(&self.uid) && !self.is_login_wayland() {
                    self.get_display_xauth_xwayland();
                } else if self.is_wayland() {
                    self.get_display_xauth_wayland();
                }
                return;
            }

            let seat0_values = get_values_of_seat0_with_gdm_wayland(&[0, 1, 2]);
            if seat0_values[0].is_empty() {
                *self = Self::default();
                return;
            }

            self.sid = seat0_values[0].clone();
            self.uid = seat0_values[1].clone();
            self.username = seat0_values[2].clone();
            self.protocol = get_display_server_of_session(&self.sid).into();
            if self.is_login_wayland() {
                self.display = "".to_owned();
                self.xauth = "".to_owned();
                // Resolve HOME even on this path. Upstream returned without it because nothing then
                // consumed a login-Wayland Desktop, but the drm build starts a `--server` as the
                // greeter uid here, and a child with no HOME has nowhere to put its config. The
                // compositor variables (WAYLAND_DISPLAY, DBUS, DISPLAY, XAUTHORITY) are left blank
                // on purpose and are NOT an oversight: the drm capture path talks to the root
                // service over `_drm` and to a render node, never to the compositor or the portal,
                // which is the entire reason it works at a login screen. `try_start_server_` skips
                // empty entries, so the greeter child simply does not get them.
                //
                // `is_login_wayland` needs `is_gdm_user(username)`, and a current GDM runs its
                // greeter as `gdm-greeter`, which that helper does not match -- measured on the
                // test host, where the greeter server therefore takes the branch below and gets a
                // fully populated environment. This is for the display managers whose greeter user
                // does match.
                #[cfg(feature = "drm")]
                self.get_home();
                return;
            }

            self.get_home();
            if self.is_wayland() {
                if is_xwayland_running(&self.uid) {
                    self.get_display_xauth_xwayland();
                } else {
                    self.get_display_xauth_wayland();
                }
            } else {
                self.get_display_x11();
                self.get_xauth_x11();
            }
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn test_desktop_env() {
            let mut d = Desktop::default();
            d.refresh();
            if d.username == "root" {
                assert_eq!(d.home, "/root");
            } else {
                if !d.username.is_empty() {
                    let home = super::super::get_env_var("HOME");
                    if !home.is_empty() {
                        assert_eq!(d.home, home);
                    } else {
                        //
                    }
                }
            }
        }
    }
}

/// A session-bus idle-inhibit interface, tried in order; the first that answers wins.
/// `org.freedesktop.ScreenSaver` is absent on purpose: that is the one `keepawake` already tried.
struct SessionInhibitTarget {
    dest: &'static str,
    path: &'static str,
    iface: &'static str,
    /// GNOME takes `(app_id, xid, reason, flags)`; PowerManagement takes `(app, reason)`.
    gnome_shape: bool,
    uninhibit: &'static str,
}

/// `org.gnome.SessionManager.Inhibit` flag 8 = idle only; logout/switch-user/suspend would take
/// away actions the person at the machine should keep.
const GNOME_INHIBIT_IDLE: u32 = 8;

const SESSION_INHIBIT_TARGETS: &[SessionInhibitTarget] = &[
    // Measured on a GDM greeter: output held 129.9 s with the inhibit, 30.3 s without.
    SessionInhibitTarget {
        dest: "org.gnome.SessionManager",
        path: "/org/gnome/SessionManager",
        iface: "org.gnome.SessionManager",
        gnome_shape: true,
        uninhibit: "Uninhibit",
    },
    // What powerdevil and xfce4-power-manager implement. NOT tested here; costs one failed call
    // where absent, and the log below names every interface tried.
    SessionInhibitTarget {
        dest: "org.freedesktop.PowerManagement",
        path: "/org/freedesktop/PowerManagement/Inhibit",
        iface: "org.freedesktop.PowerManagement.Inhibit",
        gnome_shape: false,
        uninhibit: "UnInhibit",
    },
];

/// Idle inhibit for the case `keepawake` cannot serve: it inhibits `org.freedesktop.ScreenSaver`,
/// which a GDM greeter bus neither provides nor can activate, so its `create()` fails outright.
/// The connection is kept because the inhibit is bound to it: dropping it releases the inhibit.
struct SessionIdleInhibit {
    conn: dbus::blocking::Connection,
    target: &'static SessionInhibitTarget,
    cookie: u32,
}

impl SessionIdleInhibit {
    fn new(reason: &str) -> Option<Self> {
        let conn = match dbus::blocking::Connection::new_session() {
            Ok(conn) => conn,
            Err(err) => {
                log::info!("wakelock: no session bus for the idle inhibit fallback ({err})");
                return None;
            }
        };
        let app = crate::get_app_name();
        let mut refused = Vec::new();
        for target in SESSION_INHIBIT_TARGETS {
            let res: Result<(u32,), dbus::Error> = {
                let proxy = conn.with_proxy(
                    target.dest,
                    target.path,
                    std::time::Duration::from_secs(3),
                );
                if target.gnome_shape {
                    // Inhibit(s app_id, u xid, s reason, u flags) -> u cookie; xid 0 = no window.
                    proxy.method_call(
                        target.iface,
                        "Inhibit",
                        (app.clone(), 0u32, reason.to_owned(), GNOME_INHIBIT_IDLE),
                    )
                } else {
                    proxy.method_call(target.iface, "Inhibit", (app.clone(), reason.to_owned()))
                }
            };
            match res {
                Ok((cookie,)) => {
                    log::info!(
                        "wakelock: holding a {} idle inhibit (cookie {cookie})",
                        target.dest
                    );
                    return Some(Self {
                        conn,
                        target,
                        cookie,
                    });
                }
                Err(err) => refused.push(format!("{}: {err}", target.dest)),
            }
        }
        // Name every interface tried and why it failed: on an untested desktop this log is what
        // turns "the screen still blanks" into a report naming the missing interface.
        log::info!(
            "wakelock: no session idle inhibitor answered, so the compositor may still blank this \
             screen ({})",
            refused.join("; ")
        );
        None
    }
}

impl Drop for SessionIdleInhibit {
    fn drop(&mut self) {
        let proxy = self.conn.with_proxy(
            self.target.dest,
            self.target.path,
            std::time::Duration::from_secs(3),
        );
        // Best effort: the session manager ties the inhibit to the caller's bus name, so dropping
        // `conn` below releases it even if this call does not get through.
        let res: Result<(), dbus::Error> =
            proxy.method_call(self.target.iface, self.target.uninhibit, (self.cookie,));
        if let Err(err) = res {
            log::debug!("wakelock: releasing the idle inhibit by closing the bus instead ({err})");
        }
    }
}

pub struct WakeLock(Option<keepawake::AwakeHandle>, Option<SessionIdleInhibit>);

impl WakeLock {
    pub fn new(display: bool, idle: bool, sleep: bool) -> Self {
        match keepawake::Builder::new()
            .display(display)
            .idle(idle)
            .sleep(sleep)
            .create()
        {
            Ok(handle) => WakeLock(Some(handle), None),
            Err(err) => {
                // Not `.ok()`: a discarded error is how a login screen ran with no inhibitor at
                // all and nobody noticed.
                log::info!("wakelock: keepawake could not take the inhibit ({err})");
                // keepawake asks for the ScreenSaver inhibit first and abandons the whole request
                // if it fails, losing the logind idle/sleep inhibits that stop the HOST suspending
                // mid-session. Re-ask without the display part: those are on the system bus.
                let system = if idle || sleep {
                    match keepawake::Builder::new()
                        .display(false)
                        .idle(idle)
                        .sleep(sleep)
                        .create()
                    {
                        Ok(handle) => Some(handle),
                        Err(err) => {
                            log::info!(
                                "wakelock: the logind idle/sleep inhibit did not come back \
                                 either ({err})"
                            );
                            None
                        }
                    }
                } else {
                    None
                };
                let session = if display {
                    SessionIdleInhibit::new("incoming session")
                } else {
                    None
                };
                WakeLock(system, session)
            }
        }
    }
}

fn has_cmd(cmd: &str) -> bool {
    std::process::Command::new("which")
        .arg(cmd)
        .status()
        .map(|x| x.success())
        .unwrap_or_default()
}

pub fn run_cmds_privileged(cmds: &str) -> bool {
    crate::platform::gtk_sudo::run(vec![cmds]).is_ok()
}

/// Spawn the current executable after a delay.
///
/// # Security
/// The executable path is safely quoted using `shell_quote()` to prevent
/// command injection vulnerabilities. The `secs` parameter is a u32, so it
/// cannot contain malicious input.
///
/// # Arguments
/// * `secs` - Number of seconds to wait before spawning
pub fn run_me_with(secs: u32) {
    let exe = match std::env::current_exe() {
        Ok(path) => path,
        Err(e) => {
            log::error!("Failed to get current exe: {}", e);
            return;
        }
    };

    // SECURITY: Use shell_quote to safely escape the executable path,
    // preventing command injection even if the path contains special characters.
    let exe_quoted = shell_quote(&exe.to_string_lossy());

    // Spawn a background process that sleeps and then executes.
    // The child process is automatically orphaned when parent exits,
    // and will be adopted by init (PID 1).
    Command::new(CMD_SH.as_str())
        .arg("-c")
        .arg(&format!("sleep {secs}; exec {exe_quoted}"))
        .spawn()
        .ok();
}

fn switch_service(stop: bool) -> String {
    // SECURITY: Use trusted home directory lookup via getpwuid instead of $HOME env var
    // to prevent confused-deputy attacks where an attacker manipulates environment variables.
    let home = get_home_dir_trusted()
        .map(|p| p.to_string_lossy().to_string())
        .unwrap_or_default();
    Config::set_option("stop-service".into(), if stop { "Y" } else { "" }.into());
    if !home.is_empty() && home != "/root" && !Config::get().is_empty() {
        let app_name_lower = crate::get_app_name().to_lowercase();
        let app_name0 = crate::get_app_name();
        let config_subdir = format!(".config/{}", app_name_lower);

        // SECURITY: Quote all paths to prevent shell injection from paths containing
        // spaces, semicolons, or other special characters.
        let src1 = shell_quote(&format!("{}/{}/{}.toml", home, config_subdir, app_name0));
        let src2 = shell_quote(&format!("{}/{}/{}2.toml", home, config_subdir, app_name0));
        let dst = shell_quote(&format!("/root/{}/", config_subdir));

        format!("cp -f {} {}; cp -f {} {};", src1, dst, src2, dst)
    } else {
        "".to_owned()
    }
}

pub fn uninstall_service(show_new_window: bool, _: bool) -> bool {
    if !has_cmd("systemctl") {
        // Failed when installed + flutter run + started by `show_new_window`.
        return false;
    }
    log::info!("Uninstalling service...");
    let cp = switch_service(true);
    let app_name = crate::get_app_name().to_lowercase();
    // systemctl kill rustdesk --tray, execute cp first
    if !run_cmds_privileged(&format!(
        "{cp} systemctl disable {app_name}; systemctl stop {app_name};"
    )) {
        Config::set_option("stop-service".into(), "".into());
        return true;
    }
    // systemctl stop will kill child processes, below may not be executed.
    if show_new_window {
        run_me_with(2);
    }
    std::process::exit(0);
}

pub fn install_service() -> bool {
    let _installing = crate::platform::InstallingService::new();
    if !has_cmd("systemctl") {
        return false;
    }
    log::info!("Installing service...");
    let cp = switch_service(false);
    let app_name = crate::get_app_name().to_lowercase();
    if !run_cmds_privileged(&format!(
        "{cp} systemctl enable {app_name}; systemctl start {app_name};"
    )) {
        Config::set_option("stop-service".into(), "Y".into());
    }
    true
}

fn check_if_stop_service() {
    if Config::get_option("stop-service".into()) == "Y" {
        let app_name = crate::get_app_name().to_lowercase();
        allow_err!(run_cmds(&format!(
            "systemctl disable {app_name}; systemctl stop {app_name}"
        )));
    }
}

pub fn check_autostart_config() -> ResultType<()> {
    // SECURITY: Use trusted home directory lookup via getpwuid instead of $HOME env var
    // to prevent confused-deputy attacks where an attacker manipulates environment variables.
    let home = match get_home_dir_trusted() {
        Some(p) => p.to_string_lossy().to_string(),
        None => {
            log::warn!("Failed to get trusted home directory for autostart config check");
            return Ok(());
        }
    };
    let app_name = crate::get_app_name().to_lowercase();
    let path = format!("{home}/.config/autostart");
    let file = format!("{path}/{app_name}.desktop");
    // https://github.com/rustdesk/rustdesk/issues/4863
    std::fs::remove_file(&file).ok();
    /*
        std::fs::create_dir_all(&path).ok();
        if !Path::new(&file).exists() {
            // write text to the desktop file
            let mut file = std::fs::File::create(&file)?;
            file.write_all(
                format!(
                    "
    [Desktop Entry]
    Type=Application
    Exec={app_name} --tray
    NoDisplay=false
            "
                )
                .as_bytes(),
            )?;
        }
        */
    Ok(())
}

pub struct WallPaperRemover {
    old_path: String,
    old_path_dark: Option<String>, // ubuntu 22.04 light/dark theme have different uri
}

impl WallPaperRemover {
    pub fn new() -> ResultType<Self> {
        let start = std::time::Instant::now();
        let old_path = wallpaper::get().map_err(|e| anyhow!(e.to_string()))?;
        let old_path_dark = wallpaper::get_dark().ok();
        if old_path.is_empty() && old_path_dark.clone().unwrap_or_default().is_empty() {
            bail!("already solid color");
        }
        wallpaper::set_from_path("").map_err(|e| anyhow!(e.to_string()))?;
        wallpaper::set_dark_from_path("").ok();
        log::info!(
            "created wallpaper remover,  old_path: {:?}, old_path_dark: {:?}, elapsed: {:?}",
            old_path,
            old_path_dark,
            start.elapsed(),
        );
        Ok(Self {
            old_path,
            old_path_dark,
        })
    }

    pub fn support() -> bool {
        let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
        if wallpaper::gnome::is_compliant(&desktop) || desktop.as_str() == "XFCE" {
            return wallpaper::get().is_ok();
        }
        false
    }
}

impl Drop for WallPaperRemover {
    fn drop(&mut self) {
        allow_err!(wallpaper::set_from_path(&self.old_path).map_err(|e| anyhow!(e.to_string())));
        if let Some(old_path_dark) = &self.old_path_dark {
            allow_err!(wallpaper::set_dark_from_path(old_path_dark.as_str())
                .map_err(|e| anyhow!(e.to_string())));
        }
    }
}

#[inline]
pub fn is_x11() -> bool {
    *IS_X11
}

#[inline]
pub fn is_selinux_enforcing() -> bool {
    match run_cmds("getenforce") {
        Ok(output) => output.trim() == "Enforcing",
        Err(_) => match run_cmds("sestatus") {
            Ok(output) => {
                for line in output.lines() {
                    if line.contains("Current mode:") {
                        return line.contains("enforcing");
                    }
                }
                false
            }
            Err(_) => false,
        },
    }
}

/// Get the app ID for shortcuts inhibitor permission.
/// Returns different ID based on whether running in Flatpak or native.
/// The ID must match the installed .desktop filename, as GNOME Shell's
/// inhibitShortcutsDialog uses `Shell.WindowTracker.get_window_app(window).get_id()`.
fn get_shortcuts_inhibitor_app_id() -> String {
    if is_flatpak() {
        // In Flatpak, FLATPAK_ID is set automatically by the runtime to the app ID
        // (e.g., "com.rustdesk.RustDesk"). This is the most reliable source.
        // Fall back to constructing from app name if not available.
        match std::env::var("FLATPAK_ID") {
            Ok(id) if !id.is_empty() => format!("{}.desktop", id),
            _ => {
                let app_name = crate::get_app_name();
                format!("com.{}.{}.desktop", app_name.to_lowercase(), app_name)
            }
        }
    } else {
        format!("{}.desktop", crate::get_app_name().to_lowercase())
    }
}

const PERMISSION_STORE_DEST: &str = "org.freedesktop.impl.portal.PermissionStore";
const PERMISSION_STORE_PATH: &str = "/org/freedesktop/impl/portal/PermissionStore";
const PERMISSION_STORE_IFACE: &str = "org.freedesktop.impl.portal.PermissionStore";

/// Clear GNOME shortcuts inhibitor permission via D-Bus.
/// This allows the permission dialog to be shown again.
pub fn clear_gnome_shortcuts_inhibitor_permission() -> ResultType<()> {
    let app_id = get_shortcuts_inhibitor_app_id();
    log::info!(
        "Clearing shortcuts inhibitor permission for app_id: {}, is_flatpak: {}",
        app_id,
        is_flatpak()
    );

    let conn = dbus::blocking::Connection::new_session()?;
    let proxy = conn.with_proxy(
        PERMISSION_STORE_DEST,
        PERMISSION_STORE_PATH,
        std::time::Duration::from_secs(3),
    );

    // DeletePermission(s table, s id, s app) -> ()
    let result: Result<(), dbus::Error> = proxy.method_call(
        PERMISSION_STORE_IFACE,
        "DeletePermission",
        ("gnome", "shortcuts-inhibitor", app_id.as_str()),
    );

    match result {
        Ok(()) => {
            log::info!("Successfully cleared GNOME shortcuts inhibitor permission");
            Ok(())
        }
        Err(e) => {
            let err_name = e.name().unwrap_or("");
            // If the permission doesn't exist, that's also fine
            if err_name == "org.freedesktop.portal.Error.NotFound"
                || err_name == "org.freedesktop.DBus.Error.UnknownObject"
                || err_name == "org.freedesktop.DBus.Error.ServiceUnknown"
            {
                log::info!(
                    "GNOME shortcuts inhibitor permission was not set ({})",
                    err_name
                );
                Ok(())
            } else {
                bail!("Failed to clear permission: {}", e)
            }
        }
    }
}

/// Check if GNOME shortcuts inhibitor permission exists.
pub fn has_gnome_shortcuts_inhibitor_permission() -> bool {
    let app_id = get_shortcuts_inhibitor_app_id();

    let conn = match dbus::blocking::Connection::new_session() {
        Ok(c) => c,
        Err(e) => {
            log::debug!("Failed to connect to session bus: {}", e);
            return false;
        }
    };
    let proxy = conn.with_proxy(
        PERMISSION_STORE_DEST,
        PERMISSION_STORE_PATH,
        std::time::Duration::from_secs(3),
    );

    // Lookup(s table, s id) -> (a{sas} permissions, v data)
    // We only need the permissions dict; check if app_id is a key.
    let result: Result<
        (
            std::collections::HashMap<String, Vec<String>>,
            dbus::arg::Variant<Box<dyn dbus::arg::RefArg>>,
        ),
        dbus::Error,
    > = proxy.method_call(
        PERMISSION_STORE_IFACE,
        "Lookup",
        ("gnome", "shortcuts-inhibitor"),
    );

    match result {
        Ok((permissions, _)) => {
            let found = permissions.contains_key(&app_id);
            log::debug!(
                "Shortcuts inhibitor permission lookup: app_id={}, found={}, keys={:?}",
                app_id,
                found,
                permissions.keys().collect::<Vec<_>>()
            );
            found
        }
        Err(e) => {
            log::debug!("Failed to query shortcuts inhibitor permission: {}", e);
            false
        }
    }
}
