Skip to content

Commit 68dfa08

Browse files
committed
fix: settings reopen, hotkey debounce, reregister, startup task, updater UI, version 1.0.10
1 parent c0ae13a commit 68dfa08

7 files changed

Lines changed: 273 additions & 108 deletions

File tree

.github/workflows/release.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ jobs:
3939
rustup default stable
4040
rustup target add ${{ matrix.target }}
4141
42+
- name: Set version from tag
43+
shell: pwsh
44+
run: |
45+
$ver = "${{ github.ref_name }}".TrimStart("v")
46+
$lines = Get-Content Cargo.toml
47+
$done = $false
48+
$lines = $lines | ForEach-Object {
49+
if (!$done -and $_ -match '^version\s*=\s*"') {
50+
$done = $true
51+
"version = `"$ver`""
52+
} else { $_ }
53+
}
54+
$lines | Set-Content Cargo.toml
55+
4256
- name: Cache cargo registry
4357
uses: actions/cache@v4
4458
with:
@@ -93,6 +107,20 @@ jobs:
93107
rustup update stable
94108
rustup default stable
95109
110+
- name: Set version from tag
111+
shell: pwsh
112+
run: |
113+
$ver = "${{ github.ref_name }}".TrimStart("v")
114+
$lines = Get-Content Cargo.toml
115+
$done = $false
116+
$lines = $lines | ForEach-Object {
117+
if (!$done -and $_ -match '^version\s*=\s*"') {
118+
$done = $true
119+
"version = `"$ver`""
120+
} else { $_ }
121+
}
122+
$lines | Set-Content Cargo.toml
123+
96124
- name: Cache cargo registry
97125
uses: actions/cache@v4
98126
with:

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "HideDesktopApps"
3-
version = "1.0.7"
3+
version = "1.0.10"
44
edition = "2021"
55
description = "System tray app to hide/show desktop icons, taskbar, and windows via hotkeys"
66
repository = "https://github.com/Londopy/HideDesktopApps"

src/hotkeys.rs

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -87,47 +87,43 @@ fn parse_key_code(key: &str) -> Result<Code> {
8787
Ok(code)
8888
}
8989

90-
/// Registered hotkeys with their associated IDs.
90+
/// Registered hotkeys with their IDs and the original HotKey objects (needed for unregister).
9191
pub struct RegisteredHotkeys {
92-
#[allow(dead_code)]
9392
pub manager: GlobalHotKeyManager,
9493
pub icons_id: u32,
9594
pub taskbar_id: u32,
9695
pub windows_id: u32,
96+
icons_hk: HotKey,
97+
taskbar_hk: HotKey,
98+
windows_hk: HotKey,
9799
}
98100

99-
/// Register all three hotkeys from config. Returns errors per-hotkey so partial
100-
/// registration is possible (we still function with 2/3 hotkeys working).
101+
/// Parse a hotkey string, falling back to a default and notifying on failure.
102+
fn parse_or_default(
103+
s: &str,
104+
fallback: &str,
105+
cmd_tx: &std::sync::mpsc::Sender<crate::Cmd>,
106+
) -> HotKey {
107+
match parse_hotkey(s) {
108+
Ok(hk) => hk,
109+
Err(e) => {
110+
eprintln!("Failed to parse hotkey '{}': {e}", s);
111+
let _ = cmd_tx.send(crate::Cmd::HotkeyFailed(s.to_string()));
112+
parse_hotkey(fallback).unwrap()
113+
}
114+
}
115+
}
116+
117+
/// Register all three hotkeys from config.
101118
pub fn register_hotkeys(
102119
hotkeys_config: &crate::config::HotkeysConfig,
103120
cmd_tx: &std::sync::mpsc::Sender<crate::Cmd>,
104-
) -> Result<RegisteredHotkeys> {
121+
) -> anyhow::Result<RegisteredHotkeys> {
105122
let manager = GlobalHotKeyManager::new()?;
106123

107-
let icons_hk = match parse_hotkey(&hotkeys_config.icons) {
108-
Ok(hk) => hk,
109-
Err(e) => {
110-
eprintln!("Failed to parse icons hotkey: {e}");
111-
let _ = cmd_tx.send(crate::Cmd::HotkeyFailed(hotkeys_config.icons.clone()));
112-
parse_hotkey("ctrl+alt+h").unwrap()
113-
}
114-
};
115-
let taskbar_hk = match parse_hotkey(&hotkeys_config.taskbar) {
116-
Ok(hk) => hk,
117-
Err(e) => {
118-
eprintln!("Failed to parse taskbar hotkey: {e}");
119-
let _ = cmd_tx.send(crate::Cmd::HotkeyFailed(hotkeys_config.taskbar.clone()));
120-
parse_hotkey("ctrl+alt+t").unwrap()
121-
}
122-
};
123-
let windows_hk = match parse_hotkey(&hotkeys_config.windows) {
124-
Ok(hk) => hk,
125-
Err(e) => {
126-
eprintln!("Failed to parse windows hotkey: {e}");
127-
let _ = cmd_tx.send(crate::Cmd::HotkeyFailed(hotkeys_config.windows.clone()));
128-
parse_hotkey("ctrl+alt+w").unwrap()
129-
}
130-
};
124+
let icons_hk = parse_or_default(&hotkeys_config.icons, "ctrl+alt+h", cmd_tx);
125+
let taskbar_hk = parse_or_default(&hotkeys_config.taskbar, "ctrl+alt+t", cmd_tx);
126+
let windows_hk = parse_or_default(&hotkeys_config.windows, "ctrl+alt+w", cmd_tx);
131127

132128
let icons_id = icons_hk.id();
133129
let taskbar_id = taskbar_hk.id();
@@ -151,16 +147,48 @@ pub fn register_hotkeys(
151147
icons_id,
152148
taskbar_id,
153149
windows_id,
150+
icons_hk,
151+
taskbar_hk,
152+
windows_hk,
154153
})
155154
}
156155

157-
/// Re-register hotkeys after a config change (unregister old ones first).
156+
/// Unregister old hotkeys and register new ones after a config change.
158157
pub fn reregister_hotkeys(
159-
_registered: &mut RegisteredHotkeys,
160-
_hotkeys_config: &crate::config::HotkeysConfig,
161-
_cmd_tx: &std::sync::mpsc::Sender<crate::Cmd>,
158+
registered: &mut RegisteredHotkeys,
159+
hotkeys_config: &crate::config::HotkeysConfig,
160+
cmd_tx: &std::sync::mpsc::Sender<crate::Cmd>,
162161
) {
163-
// Unregister all current hotkeys
162+
// Unregister existing hotkeys (ignore errors — they may already be gone).
163+
let _ = registered.manager.unregister(registered.icons_hk);
164+
let _ = registered.manager.unregister(registered.taskbar_hk);
165+
let _ = registered.manager.unregister(registered.windows_hk);
166+
167+
// Parse new hotkeys.
168+
let icons_hk = parse_or_default(&hotkeys_config.icons, "ctrl+alt+h", cmd_tx);
169+
let taskbar_hk = parse_or_default(&hotkeys_config.taskbar, "ctrl+alt+t", cmd_tx);
170+
let windows_hk = parse_or_default(&hotkeys_config.windows, "ctrl+alt+w", cmd_tx);
171+
172+
registered.icons_id = icons_hk.id();
173+
registered.taskbar_id = taskbar_hk.id();
174+
registered.windows_id = windows_hk.id();
175+
176+
if let Err(e) = registered.manager.register(icons_hk) {
177+
eprintln!("Re-register icons hotkey failed: {e}");
178+
let _ = cmd_tx.send(crate::Cmd::HotkeyFailed(hotkeys_config.icons.clone()));
179+
}
180+
if let Err(e) = registered.manager.register(taskbar_hk) {
181+
eprintln!("Re-register taskbar hotkey failed: {e}");
182+
let _ = cmd_tx.send(crate::Cmd::HotkeyFailed(hotkeys_config.taskbar.clone()));
183+
}
184+
if let Err(e) = registered.manager.register(windows_hk) {
185+
eprintln!("Re-register windows hotkey failed: {e}");
186+
let _ = cmd_tx.send(crate::Cmd::HotkeyFailed(hotkeys_config.windows.clone()));
187+
}
188+
189+
registered.icons_hk = icons_hk;
190+
registered.taskbar_hk = taskbar_hk;
191+
registered.windows_hk = windows_hk;
164192
}
165193

166194
/// Poll for a pending hotkey event without blocking.

src/main.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ fn main_loop(
131131
// Owned so we can swap it out when the profile list changes.
132132
let mut tray_handle = tray_handle;
133133

134+
// Debounce: Windows fires WM_HOTKEY repeatedly while a key is held, which
135+
// would cause rapid toggling. Ignore events within 400 ms of the last fire.
136+
let debounce = Duration::from_millis(400);
137+
let epoch = Instant::now()
138+
.checked_sub(debounce)
139+
.unwrap_or_else(Instant::now);
140+
let mut last_icons_fire = epoch;
141+
let mut last_taskbar_fire = epoch;
142+
let mut last_windows_fire = epoch;
143+
134144
loop {
135145
// Pump Win32 messages — tray_icon and global_hotkey both rely on a
136146
// message loop on Windows.
@@ -152,11 +162,20 @@ fn main_loop(
152162
while let Some(event) = hotkeys::poll_hotkey_event() {
153163
let id = event.id();
154164
if id == hotkey_reg.icons_id {
155-
let _ = cmd_tx.send(Cmd::ToggleIcons);
165+
if last_icons_fire.elapsed() >= debounce {
166+
last_icons_fire = Instant::now();
167+
let _ = cmd_tx.send(Cmd::ToggleIcons);
168+
}
156169
} else if id == hotkey_reg.taskbar_id {
157-
let _ = cmd_tx.send(Cmd::ToggleTaskbar);
170+
if last_taskbar_fire.elapsed() >= debounce {
171+
last_taskbar_fire = Instant::now();
172+
let _ = cmd_tx.send(Cmd::ToggleTaskbar);
173+
}
158174
} else if id == hotkey_reg.windows_id {
159-
let _ = cmd_tx.send(Cmd::ToggleWindows);
175+
if last_windows_fire.elapsed() >= debounce {
176+
last_windows_fire = Instant::now();
177+
let _ = cmd_tx.send(Cmd::ToggleWindows);
178+
}
160179
} else {
161180
// Check profile hotkeys
162181
let cfg = config_shared.lock().unwrap().clone();

src/startup.rs

Lines changed: 73 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1-
use anyhow::Result;
1+
use anyhow::{bail, Result};
2+
use std::os::windows::process::CommandExt;
23
use std::path::PathBuf;
34
use winreg::enums::*;
45
use winreg::RegKey;
56

7+
const TASK_NAME: &str = "HideDesktopApps";
68
const APP_NAME: &str = "HideDesktopApps";
7-
const RUN_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
9+
// Suppress the console window that PowerShell/schtasks would otherwise flash.
10+
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
811

9-
/// Old VBS launcher path — used only to clean up on migration.
12+
/// Run a hidden PowerShell command and return an error if it fails.
13+
fn powershell(script: &str) -> Result<()> {
14+
let status = std::process::Command::new("powershell.exe")
15+
.args([
16+
"-WindowStyle",
17+
"Hidden",
18+
"-NonInteractive",
19+
"-Command",
20+
script,
21+
])
22+
.creation_flags(CREATE_NO_WINDOW)
23+
.status()?;
24+
if !status.success() {
25+
bail!("PowerShell exited with code {:?}", status.code());
26+
}
27+
Ok(())
28+
}
29+
30+
/// Path of the old VBS launcher — cleaned up on migration.
1031
fn legacy_vbs_path() -> PathBuf {
1132
let appdata = std::env::var("APPDATA").unwrap_or_default();
1233
PathBuf::from(appdata)
@@ -18,69 +39,83 @@ fn legacy_vbs_path() -> PathBuf {
1839
.join("HideDesktopApps.vbs")
1940
}
2041

21-
/// Remove the old VBS launcher if it exists (left over from a previous install).
22-
fn remove_legacy_vbs() {
23-
let path = legacy_vbs_path();
24-
if path.exists() {
25-
let _ = std::fs::remove_file(&path);
42+
/// Remove any leftover startup entries from older installs.
43+
fn cleanup_legacy() {
44+
// Old VBS launcher
45+
let vbs = legacy_vbs_path();
46+
if vbs.exists() {
47+
let _ = std::fs::remove_file(&vbs);
48+
}
49+
// Old registry Run entry
50+
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
51+
if let Ok(key) = hkcu.open_subkey_with_flags(
52+
r"Software\Microsoft\Windows\CurrentVersion\Run",
53+
KEY_SET_VALUE,
54+
) {
55+
let _ = key.delete_value(APP_NAME);
2656
}
2757
}
2858

29-
/// Write a registry Run entry so the app launches at login.
30-
/// The exe path is quoted to handle spaces in the path.
31-
pub fn register(exe_path: &str, _delay_s: u32) -> Result<()> {
32-
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
33-
let key = hkcu.open_subkey_with_flags(RUN_KEY, KEY_SET_VALUE)?;
34-
// Quote the path so spaces are handled correctly.
35-
let value = format!("\"{}\"", exe_path);
36-
key.set_value(APP_NAME, &value)?;
37-
// Clean up the old VBS launcher if it's still around.
38-
remove_legacy_vbs();
59+
/// Register a scheduled task that runs the app at logon with an optional delay.
60+
/// Uses the same PowerShell approach as the installer so both agree on the task name.
61+
pub fn register(exe_path: &str, delay_s: u32) -> Result<()> {
62+
// ISO 8601 duration, e.g. PT30S for 30 seconds.
63+
let delay = format!("PT{}S", delay_s);
64+
// Escape single quotes inside the exe path for use in a PowerShell string.
65+
let escaped = exe_path.replace('\'', "''");
66+
let script = format!(
67+
"$t = New-ScheduledTaskTrigger -AtLogOn; \
68+
$t.Delay = '{delay}'; \
69+
$a = New-ScheduledTaskAction -Execute '\"{escaped}\"'; \
70+
$s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit 0 -AllowStartIfOnBatteries $true; \
71+
Register-ScheduledTask -TaskName '{TASK_NAME}' -Trigger $t -Action $a -Settings $s -Force | Out-Null"
72+
);
73+
powershell(&script)?;
74+
cleanup_legacy();
3975
Ok(())
4076
}
4177

42-
/// Remove the registry Run entry.
78+
/// Remove the scheduled task (and any legacy startup entries).
4379
pub fn unregister() -> Result<()> {
44-
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
45-
let key = hkcu.open_subkey_with_flags(RUN_KEY, KEY_SET_VALUE)?;
46-
// delete_value returns an error if the value doesn't exist; that's fine.
47-
let _ = key.delete_value(APP_NAME);
48-
remove_legacy_vbs();
80+
let script = format!(
81+
"Unregister-ScheduledTask -TaskName '{TASK_NAME}' -Confirm:$false -ErrorAction SilentlyContinue"
82+
);
83+
powershell(&script)?;
84+
cleanup_legacy();
4985
Ok(())
5086
}
5187

52-
/// Returns true if the registry Run entry exists.
88+
/// Returns true if the scheduled task exists.
5389
pub fn is_registered() -> bool {
54-
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
55-
if let Ok(key) = hkcu.open_subkey(RUN_KEY) {
56-
let val: Result<String, _> = key.get_value(APP_NAME);
57-
val.is_ok()
58-
} else {
59-
false
60-
}
90+
std::process::Command::new("schtasks.exe")
91+
.args(["/query", "/tn", TASK_NAME])
92+
.creation_flags(CREATE_NO_WINDOW)
93+
.output()
94+
.map(|o| o.status.success())
95+
.unwrap_or(false)
6196
}
6297

63-
/// Sync the startup entry to match the config.
98+
/// Sync the startup task to match config — called on every app startup.
6499
pub fn sync_startup(config: &crate::config::StartupConfig, exe_path: &str) {
65100
crate::dlog!("sync_startup: enabled={}, exe={}", config.enabled, exe_path);
66101
if config.enabled {
67102
match register(exe_path, config.delay_s) {
68-
Ok(()) => crate::dlog!("startup: registered OK"),
103+
Ok(()) => crate::dlog!("startup: task registered OK"),
69104
Err(e) => {
70105
crate::dlog!("startup: register failed: {}", e);
71-
eprintln!("Failed to register startup: {e}");
106+
eprintln!("Failed to register startup task: {e}");
72107
}
73108
}
74109
} else if is_registered() {
75110
match unregister() {
76-
Ok(()) => crate::dlog!("startup: unregistered OK"),
111+
Ok(()) => crate::dlog!("startup: task unregistered OK"),
77112
Err(e) => {
78113
crate::dlog!("startup: unregister failed: {}", e);
79-
eprintln!("Failed to unregister startup: {e}");
114+
eprintln!("Failed to unregister startup task: {e}");
80115
}
81116
}
82117
} else {
83-
crate::dlog!("startup: disabled and not registered, nothing to do");
118+
crate::dlog!("startup: disabled and task not present, nothing to do");
84119
}
85120
}
86121

0 commit comments

Comments
 (0)