Initial Version of Desktop Icon Hider.

This commit is contained in:
jvthompson
2026-08-08 08:56:33 -05:00
commit d29a82a741
13 changed files with 2006 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
use windows::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows::core::HRESULT;
use windows_registry::{CURRENT_USER, Result};
const RUN_KEY_PATH: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
const APP_KEY_PATH: &str = r"Software\DesktopIconHider";
const VALUE_NAME: &str = "DesktopIconHider";
const FIRST_RUN_MARKER: &str = "AutostartConfigured";
fn exe_path_quoted() -> String {
let exe_path = std::env::current_exe()
.expect("cannot resolve current exe path")
.to_string_lossy()
.into_owned();
format!("\"{exe_path}\"")
}
fn is_not_found(e: &windows::core::Error) -> bool {
e.code() == HRESULT::from_win32(ERROR_FILE_NOT_FOUND.0)
}
/// True if the Run key currently points at this exe. A missing key or value means
/// autostart is simply off (`Ok(false)`), not an error condition.
pub fn is_enabled() -> Result<bool> {
match CURRENT_USER.open(RUN_KEY_PATH) {
Ok(key) => match key.get_string(VALUE_NAME) {
Ok(value) => Ok(value == exe_path_quoted()),
Err(e) if is_not_found(&e) => Ok(false),
Err(e) => Err(e),
},
Err(e) if is_not_found(&e) => Ok(false),
Err(e) => Err(e),
}
}
/// Writes or removes the Run-key value.
pub fn set_enabled(enabled: bool) -> Result<()> {
if enabled {
CURRENT_USER
.create(RUN_KEY_PATH)?
.set_string(VALUE_NAME, exe_path_quoted())?;
Ok(())
} else {
match CURRENT_USER.open(RUN_KEY_PATH) {
Ok(key) => match key.remove_value(VALUE_NAME) {
Ok(()) => Ok(()),
Err(e) if is_not_found(&e) => Ok(()),
Err(e) => Err(e),
},
Err(e) if is_not_found(&e) => Ok(()),
Err(e) => Err(e),
}
}
}
/// On a genuine first-ever run (no marker yet under our own app key), force-enables
/// autostart once and writes the marker — so it defaults on for new installs, but never
/// re-forces itself back on after the user explicitly disables it via the tray menu.
/// Call once at startup, before reading `is_enabled()` for the menu's initial state.
pub fn ensure_first_run_default() -> Result<()> {
let app_key = CURRENT_USER.create(APP_KEY_PATH)?;
if app_key.get_u32(FIRST_RUN_MARKER).is_err() {
set_enabled(true)?;
app_key.set_u32(FIRST_RUN_MARKER, 1)?;
}
Ok(())
}
+235
View File
@@ -0,0 +1,235 @@
use std::ffi::c_void;
use std::mem::size_of;
use windows::Win32::Foundation::{CloseHandle, HWND, LPARAM, POINT, WPARAM};
use windows::Win32::Graphics::Gdi::ScreenToClient;
use windows::Win32::System::Diagnostics::Debug::{ReadProcessMemory, WriteProcessMemory};
use windows::Win32::System::Memory::{
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, VirtualAllocEx, VirtualFreeEx,
};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE,
};
use windows::Win32::UI::Controls::{LVHITTESTINFO, LVHT_NOWHERE, LVM_HITTEST};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, FindWindowExW, FindWindowW, GA_PARENT, GetAncestor, GetClassNameW,
GetWindowThreadProcessId, IsWindowVisible, SMTO_ABORTIFHUNG, SendMessageTimeoutW,
SendMessageW, WM_COMMAND, WindowFromPoint,
};
use windows::core::{BOOL, w};
/// The documented "Show desktop icons" toggle command, sent as a WM_COMMAND wParam
/// to Explorer's desktop view (the same command right-click -> View -> "Show desktop
/// icons" sends).
const TOGGLE_DESKTOP_ICONS_CMD: usize = 0x7402;
/// LVHT_ONITEMICON | LVHT_ONITEMLABEL | LVHT_ONITEMSTATEICON — not exposed as a
/// combined constant by the `windows` crate, so it's reconstructed here.
const LVHT_ONITEM_MASK: u32 = 0x000E;
/// True if the given screen point landed on the desktop's *empty* area.
///
/// When desktop icons are visible, the window under the cursor is the real desktop
/// SysListView32 (walked up through SHELLDLL_DefView -> Progman/WorkerW, to rule out
/// other SysListView32 controls such as File Explorer's file list); a list-view hit
/// test at that point must report "nowhere" rather than an actual icon.
///
/// When desktop icons are currently hidden, Explorer hides the SysListView32 window
/// itself (rather than just its items), so WindowFromPoint skips straight past it to
/// its parent SHELLDLL_DefView (or, if that's absent too, Progman/WorkerW directly).
/// In that state there are no icons to click at all, so landing on those surfaces
/// counts as empty space too — this is what lets a second double-click toggle icons
/// back on.
///
/// Any other mismatch along the way conservatively returns false so we never toggle
/// when unsure.
pub fn is_desktop_empty_space_click(screen_x: i32, screen_y: i32) -> bool {
unsafe {
let pt = POINT { x: screen_x, y: screen_y };
let hwnd = WindowFromPoint(pt);
if hwnd.is_invalid() {
return false;
}
if class_name_is(hwnd, "SysListView32") {
let parent = GetAncestor(hwnd, GA_PARENT);
if !class_name_is(parent, "SHELLDLL_DefView") {
return false;
}
let grandparent = GetAncestor(parent, GA_PARENT);
if !(class_name_is(grandparent, "Progman") || class_name_is(grandparent, "WorkerW")) {
return false;
}
let mut client_pt = pt;
if !ScreenToClient(hwnd, &mut client_pt).as_bool() {
return false;
}
match remote_hit_test(hwnd, client_pt) {
Some(info) => {
let flags = info.flags.0;
(flags & LVHT_NOWHERE.0) != 0 && (flags & LVHT_ONITEM_MASK) == 0
}
None => false,
}
} else if class_name_is(hwnd, "SHELLDLL_DefView") {
let parent = GetAncestor(hwnd, GA_PARENT);
class_name_is(parent, "Progman") || class_name_is(parent, "WorkerW")
} else {
class_name_is(hwnd, "Progman") || class_name_is(hwnd, "WorkerW")
}
}
}
/// Sends LVM_HITTEST to `hwnd`, a SysListView32 owned by another process (Explorer).
///
/// SendMessage does not marshal arbitrary struct pointers across a process boundary —
/// only a small whitelist of messages (WM_COPYDATA, WM_GETTEXT, etc.) get that
/// treatment. Passing a pointer to *our* stack directly, as an earlier version of this
/// code did, made Explorer write the hit-test result through an address that's
/// meaningless in its own address space: sometimes landing on unmapped memory (crashing
/// Explorer, which Windows then restarts) and sometimes silently corrupting or reading
/// back garbage. The correct approach is the same one automation tools use for
/// cross-process list-view access: allocate a buffer inside Explorer's own process,
/// write the input there, send the message, then read the result back.
unsafe fn remote_hit_test(hwnd: HWND, client_pt: POINT) -> Option<LVHITTESTINFO> {
unsafe {
let mut pid = 0u32;
GetWindowThreadProcessId(hwnd, Some(&mut pid));
if pid == 0 {
return None;
}
let process = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE,
false,
pid,
)
.ok()?;
let size = size_of::<LVHITTESTINFO>();
let remote_ptr = VirtualAllocEx(process, None, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if remote_ptr.is_null() {
let _ = CloseHandle(process);
return None;
}
let input = LVHITTESTINFO {
pt: client_pt,
..Default::default()
};
let result = if WriteProcessMemory(
process,
remote_ptr,
&input as *const LVHITTESTINFO as *const c_void,
size,
None,
)
.is_ok()
{
SendMessageW(
hwnd,
LVM_HITTEST,
Some(WPARAM(0)),
Some(LPARAM(remote_ptr as isize)),
);
let mut out = LVHITTESTINFO::default();
if ReadProcessMemory(
process,
remote_ptr,
&mut out as *mut LVHITTESTINFO as *mut c_void,
size,
None,
)
.is_ok()
{
Some(out)
} else {
None
}
} else {
None
};
let _ = VirtualFreeEx(process, remote_ptr, 0, MEM_RELEASE);
let _ = CloseHandle(process);
result
}
}
unsafe fn class_name_is(hwnd: HWND, expected: &str) -> bool {
if hwnd.is_invalid() {
return false;
}
let mut buf = [0u16; 256];
let len = unsafe { GetClassNameW(hwnd, &mut buf) };
if len <= 0 {
return false;
}
String::from_utf16_lossy(&buf[..len as usize]).eq_ignore_ascii_case(expected)
}
/// Sends the "Show desktop icons" toggle command to Explorer's desktop view, then reads
/// back the actual resulting visibility rather than assuming success. Returns
/// `Some(true)`/`Some(false)` for icons now shown/hidden, or `None` if the toggle
/// couldn't be delivered or confirmed. Reading back (instead of tracking our own
/// "current state") avoids drift if the user toggles icons some other way (e.g.
/// Explorer's own View menu) between our calls.
pub fn toggle_desktop_icons() -> Option<bool> {
let defview = unsafe { find_shelldll_defview() }?;
let sent = unsafe {
let mut result = 0usize;
SendMessageTimeoutW(
defview,
WM_COMMAND,
WPARAM(TOGGLE_DESKTOP_ICONS_CMD),
LPARAM(0),
SMTO_ABORTIFHUNG,
1000,
Some(&mut result),
)
};
if sent.0 == 0 {
log::warn!("toggle_desktop_icons: SendMessageTimeoutW failed or timed out");
return None;
}
// SendMessageTimeoutW blocks until Explorer's WM_COMMAND handler returns, so the
// listview's visibility already reflects the new state by now.
let listview = unsafe { FindWindowExW(Some(defview), None, w!("SysListView32"), None) }.ok()?;
Some(unsafe { IsWindowVisible(listview) }.as_bool())
}
/// Locates Explorer's desktop-view window. Normally a child of "Progman", but on some
/// multi-monitor / wallpaper-slideshow configurations Explorer reparents it under a
/// sibling "WorkerW" window instead, so that's checked as a fallback.
unsafe fn find_shelldll_defview() -> Option<HWND> {
unsafe {
if let Ok(progman) = FindWindowW(w!("Progman"), None) {
if let Ok(dv) = FindWindowExW(Some(progman), None, w!("SHELLDLL_DefView"), None) {
return Some(dv);
}
}
let mut found: Option<HWND> = None;
let _ = EnumWindows(
Some(enum_worker_w),
LPARAM(&mut found as *mut Option<HWND> as isize),
);
found
}
}
unsafe extern "system" fn enum_worker_w(hwnd: HWND, lparam: LPARAM) -> BOOL {
unsafe {
if let Ok(dv) = FindWindowExW(Some(hwnd), None, w!("SHELLDLL_DefView"), None) {
*(lparam.0 as *mut Option<HWND>) = Some(dv);
return BOOL(0); // stop enumerating
}
}
BOOL(1) // continue
}
+63
View File
@@ -0,0 +1,63 @@
use std::cell::Cell;
use std::sync::atomic::Ordering;
use windows::Win32::Foundation::{LPARAM, LRESULT, WPARAM};
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::Input::KeyboardAndMouse::{GetDoubleClickTime};
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, GetSystemMetrics, PostThreadMessageW, MSLLHOOKSTRUCT, SM_CXDOUBLECLK,
SM_CYDOUBLECLK, WM_LBUTTONDOWN,
};
use crate::state::{PAUSED, WM_APP_CANDIDATE_DBLCLICK};
thread_local! {
// (x, y, tick_ms) of the last recorded left-button-down.
static LAST_CLICK: Cell<(i32, i32, u32)> = const { Cell::new((0, 0, 0)) };
}
/// Low-level mouse hook callback (`WH_MOUSE_LL`). Runs on the thread that installed
/// the hook, for every left-button-down system-wide. Detects double-clicks manually
/// (WH_MOUSE_LL never sees a synthesized WM_LBUTTONDBLCLK) and, on a match, posts a
/// thread message so the actual hit-test/toggle work happens on the main loop instead
/// of inside this callback. Always chains via CallNextHookEx and never swallows the
/// click, so double-click-to-open still works normally on real icons.
pub unsafe extern "system" fn low_level_mouse_proc(
code: i32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if code >= 0 && wparam.0 as u32 == WM_LBUTTONDOWN && !PAUSED.load(Ordering::Relaxed) {
let info = unsafe { &*(lparam.0 as *const MSLLHOOKSTRUCT) };
let (x, y, t) = (info.pt.x, info.pt.y, info.time);
LAST_CLICK.with(|cell| {
let (lx, ly, lt) = cell.get();
let dt = t.wrapping_sub(lt);
let dx = (x - lx).abs();
let dy = (y - ly).abs();
let is_double_click = lt != 0
&& dt <= unsafe { GetDoubleClickTime() }
&& dx <= unsafe { GetSystemMetrics(SM_CXDOUBLECLK) }
&& dy <= unsafe { GetSystemMetrics(SM_CYDOUBLECLK) };
if is_double_click {
// Reset so a third click isn't misread as another double-click.
cell.set((0, 0, 0));
unsafe {
let _ = PostThreadMessageW(
GetCurrentThreadId(),
WM_APP_CANDIDATE_DBLCLICK,
WPARAM(x as usize),
LPARAM(y as isize),
);
}
} else {
cell.set((x, y, t));
}
});
}
unsafe { CallNextHookEx(None, code, wparam, lparam) }
}
+133
View File
@@ -0,0 +1,133 @@
#![windows_subsystem = "windows"]
mod autostart;
mod desktop;
mod hook;
mod state;
mod tray;
use std::sync::atomic::Ordering;
use muda::MenuEvent;
use windows::Win32::Foundation::{ERROR_ALREADY_EXISTS, GetLastError, LPARAM, WPARAM};
use windows::Win32::System::Threading::{CreateMutexW, GetCurrentThreadId};
use windows::Win32::UI::HiDpi::{
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetProcessDpiAwarenessContext,
};
use windows::Win32::UI::WindowsAndMessaging::{
DispatchMessageW, GetMessageW, PostQuitMessage, PostThreadMessageW, SetWindowsHookExW,
TranslateMessage, UnhookWindowsHookEx, MSG, WH_MOUSE_LL,
};
use windows::core::w;
fn main() {
// If another instance already holds this mutex, exit immediately — a second copy
// would install a second mouse hook, and every double-click would toggle icons
// twice (net no-op). CreateMutexW returns Ok(handle) even when the mutex already
// existed, so the only reliable signal is GetLastError(), checked unconditionally.
let _single_instance_mutex = unsafe {
let handle = CreateMutexW(None, false, w!("DesktopIconHider-9F2E7B1A-SingleInstance"));
if GetLastError() == ERROR_ALREADY_EXISTS {
return;
}
handle
};
// Must happen before any window is created (including the tray icon's hidden
// window) — otherwise Windows DPI-virtualizes our coordinate space, which
// desyncs from the raw physical-pixel coordinates the mouse hook reports
// whenever the display scale isn't 100%.
unsafe {
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
if let Err(e) = autostart::ensure_first_run_default() {
log::warn!("failed to configure autostart default: {e}");
}
let tray_handles = tray::build_tray();
let pause_item = tray_handles.pause_item.clone();
let autostart_item = tray_handles.autostart_item.clone();
let (toggle_id, pause_id, autostart_id, exit_id) = (
tray_handles.toggle_id.clone(),
tray_handles.pause_item.id().clone(),
tray_handles.autostart_item.id().clone(),
tray_handles.exit_id.clone(),
);
// muda's event handler must be Send + Sync, but MenuItem/CheckMenuItem (needed to
// update the pause item's label and correct the autostart checkbox) aren't — both
// are Rc-based. So the handler only touches ids, atomics, and plain FFI calls (the
// desktop/registry calls don't touch any Rc-based muda type), then posts a
// thread message for anything that needs the actual menu item; the main loop, which
// owns those items locally, handles the rest.
MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
if event.id == toggle_id {
desktop::toggle_desktop_icons();
} else if event.id == pause_id {
state::PAUSED.fetch_xor(true, Ordering::Relaxed);
unsafe {
let _ = PostThreadMessageW(
GetCurrentThreadId(),
state::WM_APP_PAUSE_TOGGLED,
WPARAM(0),
LPARAM(0),
);
}
} else if event.id == autostart_id {
// muda already flipped the native checkmark before this handler ran; this
// just performs the actual registry write and reports back the real result
// so the main loop can correct the checkbox if the write failed.
let desired = !autostart::is_enabled().unwrap_or(false);
if let Err(e) = autostart::set_enabled(desired) {
log::warn!("failed to update autostart: {e}");
}
let actual = autostart::is_enabled().unwrap_or(false);
unsafe {
let _ = PostThreadMessageW(
GetCurrentThreadId(),
state::WM_APP_AUTOSTART_TOGGLED,
WPARAM(actual as usize),
LPARAM(0),
);
}
} else if event.id == exit_id {
unsafe { PostQuitMessage(0) };
}
}));
let hook = unsafe { SetWindowsHookExW(WH_MOUSE_LL, Some(hook::low_level_mouse_proc), None, 0) }
.expect("failed to install low-level mouse hook");
let mut msg = MSG::default();
unsafe {
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
if msg.message == state::WM_APP_CANDIDATE_DBLCLICK {
let x = msg.wParam.0 as i32;
let y = msg.lParam.0 as i32;
if !state::PAUSED.load(Ordering::Relaxed)
&& desktop::is_desktop_empty_space_click(x, y)
{
desktop::toggle_desktop_icons();
}
} else if msg.message == state::WM_APP_PAUSE_TOGGLED {
let now_paused = state::PAUSED.load(Ordering::Relaxed);
pause_item.set_text(if now_paused {
"Unpause Monitoring"
} else {
"Pause Monitoring"
});
} else if msg.message == state::WM_APP_AUTOSTART_TOGGLED {
let actual = msg.wParam.0 != 0;
autostart_item.set_checked(actual);
} else {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
let _ = UnhookWindowsHookEx(hook);
}
drop(tray_handles.tray);
}
+20
View File
@@ -0,0 +1,20 @@
use std::sync::atomic::AtomicBool;
use windows::Win32::UI::WindowsAndMessaging::WM_APP;
/// When true, double-click monitoring is temporarily disabled (tray menu toggle).
pub static PAUSED: AtomicBool = AtomicBool::new(false);
/// Posted from the mouse-hook callback to the main thread's message loop when a
/// candidate double-click is detected. WPARAM = screen x, LPARAM = screen y.
/// Kept out of the hook callback itself so the hook stays cheap.
pub const WM_APP_CANDIDATE_DBLCLICK: u32 = WM_APP + 1;
/// Posted from the (Send + Sync-bound) menu event handler when the pause state
/// changes, so the main loop — which owns the actual (non-Send) MenuItem — can update
/// its label without needing to move the item into that handler.
pub const WM_APP_PAUSE_TOGGLED: u32 = WM_APP + 2;
/// Posted from the menu event handler after an autostart registry write, carrying the
/// actual resulting enabled state in wParam. muda already flips the native checkmark
/// itself on click, so the main loop only needs to correct it if the write failed.
pub const WM_APP_AUTOSTART_TOGGLED: u32 = WM_APP + 3;
+57
View File
@@ -0,0 +1,57 @@
use muda::{CheckMenuItem, Menu, MenuId, MenuItem};
use tray_icon::{Icon, TrayIcon, TrayIconBuilder};
use crate::autostart;
const TRAY_PNG: &[u8] = include_bytes!("../assets/tray-32.png");
pub struct TrayHandles {
// Kept alive for as long as the tray icon should remain visible.
pub tray: TrayIcon,
pub toggle_id: MenuId,
// Kept as the full item (not just its id) so its label can be flipped between
// "Pause Monitoring" / "Unpause Monitoring" as the paused state changes.
pub pause_item: MenuItem,
// Kept as the full item so its checked state can be corrected if a registry write
// fails (muda already flips the native checkmark itself on click).
pub autostart_item: CheckMenuItem,
pub exit_id: MenuId,
}
pub fn build_tray() -> TrayHandles {
let icon = load_icon();
let autostart_enabled = autostart::is_enabled().unwrap_or(false);
let menu = Menu::new();
let toggle_item = MenuItem::new("Toggle Icons Now", true, None);
let pause_item = MenuItem::new("Pause Monitoring", true, None);
let autostart_item = CheckMenuItem::new("Start with Windows", true, autostart_enabled, None);
let exit_item = MenuItem::new("Exit", true, None);
menu.append(&toggle_item).expect("failed to append menu item");
menu.append(&pause_item).expect("failed to append menu item");
menu.append(&autostart_item).expect("failed to append menu item");
menu.append(&exit_item).expect("failed to append menu item");
let tray = TrayIconBuilder::new()
.with_icon(icon)
.with_menu(Box::new(menu))
.with_tooltip("Desktop Icon Hider")
.build()
.expect("failed to create tray icon");
TrayHandles {
tray,
toggle_id: toggle_item.id().clone(),
pause_item,
autostart_item,
exit_id: exit_item.id().clone(),
}
}
fn load_icon() -> Icon {
let img = image::load_from_memory(TRAY_PNG)
.expect("embedded tray icon is invalid")
.into_rgba8();
let (width, height) = img.dimensions();
Icon::from_rgba(img.into_raw(), width, height).expect("failed to build tray icon")
}