Skip to content

Commit 87b2f64

Browse files
committed
feat: single-instance guard, hotkey recorder, settings status header
1 parent bc47cae commit 87b2f64

4 files changed

Lines changed: 276 additions & 51 deletions

File tree

src/main.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ pub enum Cmd {
3939
}
4040

4141
fn main() {
42+
// Single-instance guard: if another copy is already running, exit quietly.
43+
// A restart relaunch passes --restarted so it waits for the old one to exit.
44+
let restarted = std::env::args().any(|a| a == "--restarted");
45+
let _instance = match win_ops::acquire_single_instance(restarted) {
46+
Some(guard) => guard,
47+
None => return,
48+
};
49+
4250
// if we panic, try to restore icons/taskbar before crashing
4351
let default_hook = std::panic::take_hook();
4452
std::panic::set_hook(Box::new(move |info| {
@@ -379,7 +387,11 @@ fn main_loop(
379387
Cmd::OpenSettings => {
380388
dlog!("Cmd::OpenSettings received");
381389
// runs in a background thread, main loop keeps going
382-
ui::open_settings(Arc::clone(&config_shared), cmd_tx.clone());
390+
ui::open_settings(
391+
Arc::clone(&config_shared),
392+
Arc::clone(&state_shared),
393+
cmd_tx.clone(),
394+
);
383395
}
384396

385397
Cmd::Restart => {
@@ -389,7 +401,7 @@ fn main_loop(
389401
}
390402
let exe = std::env::current_exe()
391403
.unwrap_or_else(|_| std::path::PathBuf::from("HideDesktopApps.exe"));
392-
let _ = std::process::Command::new(exe).spawn();
404+
let _ = std::process::Command::new(exe).arg("--restarted").spawn();
393405
return Ok(());
394406
}
395407

src/ui/hotkeys_tab.rs

Lines changed: 165 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::SettingsApp;
1+
use super::{HotkeyField, SettingsApp};
22
use egui::Ui;
33

44
// split a hotkey string so we can show it in the editor
@@ -36,61 +36,180 @@ fn build_hotkey(ctrl: bool, alt: bool, shift: bool, win: bool, key: &str) -> Str
3636
parts.join("+")
3737
}
3838

39-
// renders a hotkey editor for one action
40-
fn hotkey_editor(ui: &mut Ui, label: &str, current: &str) -> String {
41-
let (mut ctrl, mut alt, mut shift, mut win, mut key) = parse_for_edit(current);
42-
43-
ui.group(|ui| {
44-
ui.label(label);
45-
ui.horizontal(|ui| {
46-
ui.checkbox(&mut ctrl, "Ctrl");
47-
ui.checkbox(&mut alt, "Alt");
48-
ui.checkbox(&mut shift, "Shift");
49-
ui.checkbox(&mut win, "Win");
50-
});
51-
ui.horizontal(|ui| {
52-
ui.label("Key:");
53-
// Only allow a single A-Z character
54-
let mut key_input = key.clone();
55-
let response = ui.add(
56-
egui::TextEdit::singleline(&mut key_input)
57-
.desired_width(40.0)
58-
.char_limit(1),
59-
);
60-
if response.changed() {
61-
let filtered: String = key_input
62-
.chars()
63-
.filter(|c| c.is_ascii_alphabetic())
64-
.map(|c| c.to_ascii_uppercase())
65-
.collect();
66-
key = filtered;
39+
// map an egui key to the token our parser understands (a-z, 0-9, f1-f12)
40+
fn egui_key_token(key: egui::Key) -> Option<&'static str> {
41+
use egui::Key::*;
42+
Some(match key {
43+
A => "a",
44+
B => "b",
45+
C => "c",
46+
D => "d",
47+
E => "e",
48+
F => "f",
49+
G => "g",
50+
H => "h",
51+
I => "i",
52+
J => "j",
53+
K => "k",
54+
L => "l",
55+
M => "m",
56+
N => "n",
57+
O => "o",
58+
P => "p",
59+
Q => "q",
60+
R => "r",
61+
S => "s",
62+
T => "t",
63+
U => "u",
64+
V => "v",
65+
W => "w",
66+
X => "x",
67+
Y => "y",
68+
Z => "z",
69+
Num0 => "0",
70+
Num1 => "1",
71+
Num2 => "2",
72+
Num3 => "3",
73+
Num4 => "4",
74+
Num5 => "5",
75+
Num6 => "6",
76+
Num7 => "7",
77+
Num8 => "8",
78+
Num9 => "9",
79+
F1 => "f1",
80+
F2 => "f2",
81+
F3 => "f3",
82+
F4 => "f4",
83+
F5 => "f5",
84+
F6 => "f6",
85+
F7 => "f7",
86+
F8 => "f8",
87+
F9 => "f9",
88+
F10 => "f10",
89+
F11 => "f11",
90+
F12 => "f12",
91+
_ => return None,
92+
})
93+
}
94+
95+
// look for a "modifier(s)+key" press in this frame's events
96+
fn capture_combo(input: &egui::InputState) -> Option<String> {
97+
for ev in &input.events {
98+
if let egui::Event::Key { key, pressed: true, modifiers, .. } = ev {
99+
if let Some(tok) = egui_key_token(*key) {
100+
let mut parts: Vec<&str> = Vec::new();
101+
if modifiers.ctrl {
102+
parts.push("ctrl");
103+
}
104+
if modifiers.alt {
105+
parts.push("alt");
106+
}
107+
if modifiers.shift {
108+
parts.push("shift");
109+
}
110+
// require at least one modifier (a bare key is not a valid hotkey)
111+
if !parts.is_empty() {
112+
parts.push(tok);
113+
return Some(parts.join("+"));
114+
}
67115
}
68-
let preview = build_hotkey(ctrl, alt, shift, win, &key.to_lowercase());
69-
ui.label(format!("→ {}", preview));
70-
});
71-
});
116+
}
117+
}
118+
None
119+
}
72120

73-
build_hotkey(ctrl, alt, shift, win, &key.to_lowercase())
121+
fn field_label(field: HotkeyField) -> &'static str {
122+
match field {
123+
HotkeyField::Icons => "Toggle Desktop Icons",
124+
HotkeyField::Taskbar => "Toggle Taskbar",
125+
HotkeyField::Windows => "Toggle App Windows",
126+
}
74127
}
75128

76129
impl SettingsApp {
130+
// a hotkey editor (checkboxes + key, or a Record button) for one action
131+
fn hotkey_editor(&mut self, ui: &mut Ui, field: HotkeyField, current: &str) -> String {
132+
let (mut ctrl, mut alt, mut shift, mut win, mut key) = parse_for_edit(current);
133+
let recording = self.recording_hotkey == Some(field);
134+
135+
// if recording this field, try to capture a combo from this frame's input
136+
let mut captured: Option<String> = None;
137+
if recording {
138+
ui.ctx().request_repaint();
139+
if let Some(combo) = ui.input(capture_combo) {
140+
captured = Some(combo);
141+
self.recording_hotkey = None;
142+
}
143+
}
144+
145+
let mut toggle_record = false;
146+
ui.group(|ui| {
147+
ui.horizontal(|ui| {
148+
ui.label(field_label(field));
149+
let btn = if recording {
150+
"Press a combo\u{2026} (Esc to cancel)"
151+
} else {
152+
"\u{2328} Record"
153+
};
154+
if ui.button(btn).clicked() {
155+
toggle_record = true;
156+
}
157+
});
158+
ui.horizontal(|ui| {
159+
ui.checkbox(&mut ctrl, "Ctrl");
160+
ui.checkbox(&mut alt, "Alt");
161+
ui.checkbox(&mut shift, "Shift");
162+
ui.checkbox(&mut win, "Win");
163+
});
164+
ui.horizontal(|ui| {
165+
ui.label("Key:");
166+
let mut key_input = key.clone();
167+
let response = ui.add(
168+
egui::TextEdit::singleline(&mut key_input)
169+
.desired_width(40.0)
170+
.char_limit(1),
171+
);
172+
if response.changed() {
173+
key = key_input
174+
.chars()
175+
.filter(|c| c.is_ascii_alphabetic())
176+
.map(|c| c.to_ascii_uppercase())
177+
.collect();
178+
}
179+
let preview = build_hotkey(ctrl, alt, shift, win, &key.to_lowercase());
180+
ui.label(format!("\u{2192} {}", preview));
181+
});
182+
});
183+
184+
if toggle_record {
185+
self.recording_hotkey = if recording { None } else { Some(field) };
186+
}
187+
188+
if let Some(combo) = captured {
189+
return combo;
190+
}
191+
build_hotkey(ctrl, alt, shift, win, &key.to_lowercase())
192+
}
193+
77194
pub fn hotkeys_tab(&mut self, ui: &mut Ui) {
78195
ui.heading("Global Hotkeys");
196+
ui.label("Click Record and press your combo, or use the checkboxes + key.");
197+
ui.weak("Defaults \u{2014} Icons: Ctrl+Alt+H, Taskbar: Ctrl+Alt+T, Windows: Ctrl+Alt+W");
79198
ui.add_space(8.0);
80199

81-
let new_icons = hotkey_editor(
82-
ui,
83-
"Toggle Desktop Icons",
84-
&self.config.hotkeys.icons.clone(),
85-
);
200+
// Esc cancels an in-progress recording
201+
if self.recording_hotkey.is_some() && ui.input(|i| i.key_pressed(egui::Key::Escape)) {
202+
self.recording_hotkey = None;
203+
}
204+
205+
let icons = self.config.hotkeys.icons.clone();
206+
let new_icons = self.hotkey_editor(ui, HotkeyField::Icons, &icons);
86207
ui.add_space(4.0);
87-
let new_taskbar = hotkey_editor(ui, "Toggle Taskbar", &self.config.hotkeys.taskbar.clone());
208+
let taskbar = self.config.hotkeys.taskbar.clone();
209+
let new_taskbar = self.hotkey_editor(ui, HotkeyField::Taskbar, &taskbar);
88210
ui.add_space(4.0);
89-
let new_windows = hotkey_editor(
90-
ui,
91-
"Toggle App Windows",
92-
&self.config.hotkeys.windows.clone(),
93-
);
211+
let windows = self.config.hotkeys.windows.clone();
212+
let new_windows = self.hotkey_editor(ui, HotkeyField::Windows, &windows);
94213

95214
if new_icons != self.config.hotkeys.icons
96215
|| new_taskbar != self.config.hotkeys.taskbar
@@ -107,7 +226,7 @@ impl SettingsApp {
107226
if h.icons == h.taskbar || h.icons == h.windows || h.taskbar == h.windows {
108227
ui.colored_label(
109228
egui::Color32::YELLOW,
110-
"Warning: duplicate hotkeys all three must be unique.",
229+
"Warning: duplicate hotkeys \u{2014} all three must be unique.",
111230
);
112231
}
113232
}

src/ui/mod.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,24 @@ impl Tab {
5454
}
5555
}
5656

57+
// which hotkey field the recorder is currently capturing
58+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59+
pub enum HotkeyField {
60+
Icons,
61+
Taskbar,
62+
Windows,
63+
}
64+
65+
// a small colored "shown/hidden" chip for the status header
66+
fn status_chip(ui: &mut egui::Ui, name: &str, hidden: bool) {
67+
let (text, color) = if hidden {
68+
(format!("{name}: hidden"), egui::Color32::from_rgb(231, 76, 60))
69+
} else {
70+
(format!("{name}: shown"), egui::Color32::from_rgb(46, 204, 113))
71+
};
72+
ui.colored_label(color, text);
73+
}
74+
5775
pub struct SettingsApp {
5876
pub config: AppConfig,
5977
pub config_shared: Arc<Mutex<AppConfig>>,
@@ -66,10 +84,18 @@ pub struct SettingsApp {
6684
pub startup_error: Option<String>,
6785
/// Set to true by any tab that modifies config; triggers a save at end of frame.
6886
pub dirty: bool,
87+
/// Live app state, for the status header.
88+
pub state_shared: Arc<Mutex<crate::state::AppState>>,
89+
/// Which hotkey field (if any) is currently being recorded.
90+
pub recording_hotkey: Option<HotkeyField>,
6991
}
7092

7193
impl SettingsApp {
72-
pub fn new(config_shared: Arc<Mutex<AppConfig>>, cmd_tx: mpsc::Sender<Cmd>) -> Self {
94+
pub fn new(
95+
config_shared: Arc<Mutex<AppConfig>>,
96+
state_shared: Arc<Mutex<crate::state::AppState>>,
97+
cmd_tx: mpsc::Sender<Cmd>,
98+
) -> Self {
7399
let config = config_shared.lock().unwrap().clone();
74100
let startup_registered = crate::startup::is_registered();
75101
Self {
@@ -82,6 +108,8 @@ impl SettingsApp {
82108
startup_registered,
83109
startup_error: None,
84110
dirty: false,
111+
state_shared,
112+
recording_hotkey: None,
85113
}
86114
}
87115

@@ -115,7 +143,26 @@ impl eframe::App for SettingsApp {
115143
return;
116144
}
117145

146+
// keep the live status header fresh while the window is open
147+
ctx.request_repaint_after(std::time::Duration::from_millis(500));
148+
118149
egui::CentralPanel::default().show(ctx, |ui| {
150+
// live status of what's currently hidden
151+
{
152+
let st = self.state_shared.lock().unwrap();
153+
ui.horizontal(|ui| {
154+
ui.label("Status:");
155+
status_chip(ui, "Icons", st.icons_hidden);
156+
status_chip(ui, "Taskbar", st.taskbar_hidden);
157+
status_chip(ui, "Windows", st.windows_hidden);
158+
if let Some(p) = &st.active_profile {
159+
ui.separator();
160+
ui.label(format!("Profile: {p}"));
161+
}
162+
});
163+
}
164+
ui.separator();
165+
119166
ui.horizontal(|ui| {
120167
for &tab in Tab::all() {
121168
ui.selectable_value(&mut self.current_tab, tab, tab.label());
@@ -147,7 +194,11 @@ impl eframe::App for SettingsApp {
147194
}
148195

149196
// open the settings window, or restore it if it's already running
150-
pub fn open_settings(config_shared: Arc<Mutex<AppConfig>>, cmd_tx: mpsc::Sender<Cmd>) {
197+
pub fn open_settings(
198+
config_shared: Arc<Mutex<AppConfig>>,
199+
state_shared: Arc<Mutex<crate::state::AppState>>,
200+
cmd_tx: mpsc::Sender<Cmd>,
201+
) {
151202
// already running, just tell it to show itself
152203
let existing = SETTINGS_CTX.lock().unwrap().clone();
153204
if let Some(ctx) = existing {
@@ -198,7 +249,7 @@ pub fn open_settings(config_shared: Arc<Mutex<AppConfig>>, cmd_tx: mpsc::Sender<
198249
Box::new(move |cc| {
199250
// save the context so open_settings can wake us up later
200251
*SETTINGS_CTX.lock().unwrap() = Some(cc.egui_ctx.clone());
201-
Ok(Box::new(SettingsApp::new(config_shared, cmd_tx)))
252+
Ok(Box::new(SettingsApp::new(config_shared, state_shared, cmd_tx)))
202253
}),
203254
);
204255

0 commit comments

Comments
 (0)