Initial Version of Desktop Icon Hider.
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(cargo --version)",
|
||||||
|
"Bash(rustc --version)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+1377
File diff suppressed because it is too large
Load Diff
+38
@@ -0,0 +1,38 @@
|
|||||||
|
[package]
|
||||||
|
name = "desktop-icon-hider"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "desktop-icon-hider"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tray-icon = "0.24"
|
||||||
|
muda = "0.19"
|
||||||
|
windows-registry = "0.6"
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
[dependencies.windows]
|
||||||
|
version = "0.62"
|
||||||
|
features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
"Win32_UI_Controls",
|
||||||
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_System_Memory",
|
||||||
|
"Win32_System_Diagnostics_Debug",
|
||||||
|
"Win32_Graphics_Gdi",
|
||||||
|
"Win32_UI_HiDpi",
|
||||||
|
"Win32_Security",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
winresource = "0.1"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "s"
|
||||||
|
lto = true
|
||||||
|
strip = true
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 998 B |
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
fn main() {
|
||||||
|
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
|
||||||
|
let mut res = winresource::WindowsResource::new();
|
||||||
|
res.set_icon("assets/tray.ico");
|
||||||
|
res.compile().expect("failed to embed icon resource");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user