focus-tracker 1.1.0

Cross-platform focus tracker for Linux (X11), macOS and Windows
Documentation
use crate::{FocusTrackerError, FocusTrackerResult};
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::{
    Foundation::{CloseHandle, HANDLE, HWND},
    System::{
        ProcessStatus::GetModuleBaseNameW,
        Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ},
    },
    UI::WindowsAndMessaging::{
        GetForegroundWindow, GetWindowTextW, GetWindowThreadProcessId, IsWindow,
    },
};

pub(crate) struct HandleGuard(pub HANDLE);

impl Drop for HandleGuard {
    fn drop(&mut self) {
        unsafe { CloseHandle(self.0) };
    }
}

pub fn get_foreground_window() -> Option<HWND> {
    let hwnd = unsafe { GetForegroundWindow() };
    if hwnd.is_null() || unsafe { IsWindow(hwnd) } == 0 {
        None
    } else {
        Some(hwnd)
    }
}

pub fn is_interactive_session() -> FocusTrackerResult<bool> {
    use windows_sys::Win32::System::StationsAndDesktops::{
        GetProcessWindowStation, GetUserObjectInformationW, UOI_FLAGS, USEROBJECTFLAGS,
    };

    let station = unsafe { GetProcessWindowStation() };
    if station.is_null() {
        return Err(FocusTrackerError::platform(
            "failed to get process window station",
        ));
    }

    let mut flags: USEROBJECTFLAGS = unsafe { std::mem::zeroed() };
    let mut needed: u32 = 0;
    let ok = unsafe {
        GetUserObjectInformationW(
            station as _,
            UOI_FLAGS,
            &mut flags as *mut _ as *mut _,
            std::mem::size_of::<USEROBJECTFLAGS>() as u32,
            &mut needed,
        )
    };

    if ok == 0 {
        return Err(FocusTrackerError::platform(
            "failed to get window station flags",
        ));
    }

    Ok(flags.dwFlags & 1 != 0)
}

pub(crate) fn get_window_title(hwnd: HWND) -> FocusTrackerResult<String> {
    let mut buffer = [0u16; 512];
    let len = unsafe { GetWindowTextW(hwnd, buffer.as_mut_ptr(), buffer.len() as i32) };

    if len == 0 {
        return Ok(String::new());
    }

    let title = OsString::from_wide(&buffer[..len as usize])
        .to_string_lossy()
        .into_owned();

    Ok(title)
}

pub(crate) fn get_window_process_id(hwnd: HWND) -> FocusTrackerResult<u32> {
    let mut process_id = 0u32;
    unsafe {
        GetWindowThreadProcessId(hwnd, &mut process_id);
    }

    if process_id == 0 {
        return Err(FocusTrackerError::platform("failed to get process ID"));
    }

    Ok(process_id)
}

pub(crate) fn get_process_name(process_id: u32) -> FocusTrackerResult<String> {
    let process_handle =
        unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0, process_id) };

    if process_handle.is_null() {
        return Err(FocusTrackerError::platform("failed to open process"));
    }

    let _guard = HandleGuard(process_handle);

    let mut buffer = [0u16; 512];
    let len = unsafe {
        GetModuleBaseNameW(
            process_handle,
            std::ptr::null_mut(),
            buffer.as_mut_ptr(),
            buffer.len() as u32,
        )
    };

    if len == 0 {
        return Err(FocusTrackerError::platform("failed to get module name"));
    }

    let name = OsString::from_wide(&buffer[..len as usize])
        .to_string_lossy()
        .into_owned();

    Ok(name)
}

pub(crate) fn get_process_exe_path(process_id: u32) -> FocusTrackerResult<Vec<u16>> {
    use windows_sys::Win32::System::Threading::{PROCESS_NAME_WIN32, QueryFullProcessImageNameW};

    let process_handle = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION, 0, process_id) };

    if process_handle.is_null() {
        return Err(FocusTrackerError::platform("failed to open process"));
    }

    let _guard = HandleGuard(process_handle);

    let mut buffer = vec![0u16; 32768];
    let mut len = buffer.len() as u32;
    let ok = unsafe {
        QueryFullProcessImageNameW(
            process_handle,
            PROCESS_NAME_WIN32,
            buffer.as_mut_ptr(),
            &mut len,
        )
    };

    if ok == 0 || len == 0 {
        return Err(FocusTrackerError::platform(
            "failed to query process image name",
        ));
    }

    buffer.truncate(len as usize);
    Ok(buffer)
}

pub(crate) fn get_window_info(hwnd: HWND) -> FocusTrackerResult<(Option<String>, String)> {
    let title = get_window_title(hwnd).unwrap_or_default();
    let title = if title.is_empty() { None } else { Some(title) };
    let process_id = get_window_process_id(hwnd)?;
    let process_name =
        get_process_name(process_id).unwrap_or_else(|_| format!("Process_{}", process_id));

    Ok((title, process_name))
}