Skip to content

Commit bc5ee35

Browse files
committed
fix: Improve hotkey system and shorten feedback messages
- Remove dead code for shifted character normalization (28 lines) - Shorten all pill window feedback messages to 3-4 words max - Add comprehensive system hotkey conflict detection - Warn users about OS-reserved key combinations - Add device name to license API calls - Clean up backend key normalizer for better maintainability
1 parent 43dca35 commit bc5ee35

File tree

8 files changed

+200
-45
lines changed

8 files changed

+200
-45
lines changed

src-tauri/src/commands/audio.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ pub async fn start_recording(
268268

269269
// Emit user-friendly error
270270
let _ = emit_to_window(&app, "pill", "recording-error",
271-
"Could not access microphone. Please check your audio settings and permissions.");
271+
"Microphone access failed");
272272

273273
return Err("Failed to start recording".to_string());
274274
} else {
@@ -295,13 +295,13 @@ pub async fn start_recording(
295295

296296
// Provide specific error messages for common issues
297297
let user_message = if e.contains("permission") || e.contains("access") {
298-
"Microphone access denied. Please grant permission in System Preferences."
298+
"Microphone permission denied"
299299
} else if e.contains("device") || e.contains("not found") {
300-
"No microphone found. Please connect a microphone and try again."
300+
"No microphone found"
301301
} else if e.contains("in use") || e.contains("busy") {
302-
"Microphone is being used by another application. Please close other recording apps."
302+
"Microphone busy"
303303
} else {
304-
"Could not start recording. Please check your audio settings."
304+
"Recording failed"
305305
};
306306

307307
let _ = emit_to_window(&app, "pill", "recording-error", user_message);
@@ -990,7 +990,7 @@ pub async fn stop_recording(
990990
&app_for_process,
991991
"pill",
992992
"paste-error",
993-
"Text copied to clipboard. Grant accessibility permission to auto-paste."
993+
"Text copied - grant permission to auto-paste"
994994
);
995995

996996
// Keep pill visible for 3 seconds with error
@@ -1006,7 +1006,7 @@ pub async fn stop_recording(
10061006
&app_for_process,
10071007
"main",
10081008
"paste-error",
1009-
format!("Failed to paste text: {}. Text is in clipboard.", e),
1009+
format!("Paste failed - text in clipboard"),
10101010
);
10111011
}
10121012
}

src-tauri/src/commands/key_normalizer.rs

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,36 +28,9 @@ fn normalize_single_key(key: &str) -> String {
2828
_ => {}
2929
}
3030

31-
// Handle shifted characters - convert them back to their base keys
32-
// This is needed because frontend might capture shifted characters
33-
// Complete mapping for US/UK keyboard shifted characters
31+
// Normalize common punctuation to their Code enum names
32+
// This handles cases where the frontend sends the actual character instead of the Code name
3433
match key {
35-
// Shifted punctuation
36-
"<" => return "Comma".to_string(),
37-
">" => return "Period".to_string(),
38-
"?" => return "Slash".to_string(),
39-
":" => return "Semicolon".to_string(),
40-
"\"" => return "Quote".to_string(),
41-
"{" => return "BracketLeft".to_string(),
42-
"}" => return "BracketRight".to_string(),
43-
"|" => return "Backslash".to_string(),
44-
"+" => return "Equal".to_string(),
45-
"_" => return "Minus".to_string(),
46-
"~" => return "Backquote".to_string(),
47-
48-
// Shifted numbers (top row)
49-
"!" => return "Digit1".to_string(),
50-
"@" => return "Digit2".to_string(),
51-
"#" => return "Digit3".to_string(),
52-
"$" => return "Digit4".to_string(),
53-
"%" => return "Digit5".to_string(),
54-
"^" => return "Digit6".to_string(),
55-
"&" => return "Digit7".to_string(),
56-
"*" => return "Digit8".to_string(),
57-
"(" => return "Digit9".to_string(),
58-
")" => return "Digit0".to_string(),
59-
60-
// Common punctuation (unshifted) - normalize these too
6134
"," => return "Comma".to_string(),
6235
"." => return "Period".to_string(),
6336
"/" => return "Slash".to_string(),
@@ -69,7 +42,6 @@ fn normalize_single_key(key: &str) -> String {
6942
"=" => return "Equal".to_string(),
7043
"-" => return "Minus".to_string(),
7144
"`" => return "Backquote".to_string(),
72-
7345
_ => {}
7446
}
7547

src-tauri/src/license/api_client.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ impl LicenseApiClient {
168168
body["osVersion"] = json!(os_version);
169169
}
170170

171+
// Add device name (hostname)
172+
if let Some(device_name) = System::host_name() {
173+
body["deviceName"] = json!(device_name);
174+
}
175+
171176
let response = self
172177
.client
173178
.post(&url)

src-tauri/src/whisper/transcriber.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ impl Transcriber {
461461

462462
// Check minimum duration (0.5 seconds)
463463
if duration_seconds < 0.5 {
464-
let error = format!("Recording too short ({:.1}s). Minimum duration is 0.5 seconds", duration_seconds);
464+
let error = format!("Recording too short");
465465
log::warn!("[TRANSCRIPTION_DEBUG] {}", error);
466466
return Err(error);
467467
}

src/components/HotkeyInput.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ValidationPresets
1212
} from "@/lib/keyboard-normalizer";
1313
import { mapCodeToKey } from "@/lib/keyboard-mapper";
14+
import { checkForSystemConflict, formatConflictMessage } from "@/lib/hotkey-conflicts";
1415

1516
interface HotkeyInputProps {
1617
value: string;
@@ -147,8 +148,20 @@ export const HotkeyInput = React.memo(function HotkeyInput({
147148
setValidationError(validation.error || "Invalid key combination");
148149
setPendingHotkey("");
149150
} else {
150-
setPendingHotkey(shortcut);
151-
setValidationError("");
151+
// Check for system conflicts
152+
const conflict = checkForSystemConflict(shortcut);
153+
if (conflict) {
154+
setValidationError(formatConflictMessage(conflict));
155+
// Still show the hotkey but with warning for 'warning' severity
156+
if (conflict.severity === 'warning') {
157+
setPendingHotkey(shortcut);
158+
} else {
159+
setPendingHotkey("");
160+
}
161+
} else {
162+
setPendingHotkey(shortcut);
163+
setValidationError("");
164+
}
152165
}
153166
}
154167
};
@@ -178,9 +191,22 @@ export const HotkeyInput = React.memo(function HotkeyInput({
178191

179192
const validation = validateKeyCombinationWithRules(shortcut, validationRules);
180193
if (validation.valid) {
181-
setPendingHotkey(shortcut);
182-
setKeys(new Set());
183-
setCurrentKeysDisplay("");
194+
// Check for system conflicts
195+
const conflict = checkForSystemConflict(shortcut);
196+
if (conflict) {
197+
setValidationError(formatConflictMessage(conflict));
198+
// Still allow setting it, but with warning
199+
if (conflict.severity === 'warning') {
200+
setPendingHotkey(shortcut);
201+
setKeys(new Set());
202+
setCurrentKeysDisplay("");
203+
}
204+
} else {
205+
setPendingHotkey(shortcut);
206+
setKeys(new Set());
207+
setCurrentKeysDisplay("");
208+
setValidationError("");
209+
}
184210
} else {
185211
setValidationError(validation.error || "Invalid key combination");
186212
}

src/components/RecordingPill.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export function RecordingPill() {
109109
unlisteners.push(
110110
listen("recording-stopped-silence", () => {
111111
console.log("RecordingPill: Received recording-stopped-silence event");
112-
setFeedbackWithTimeout("Recording stopped - no sound detected", 2000);
112+
setFeedbackWithTimeout("No sound detected", 2000);
113113
})
114114
);
115115

src/contexts/LicenseContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export function LicenseProvider({ children }: { children: ReactNode }) {
7979
await invoke('deactivate_license');
8080
// Re-check status after deactivation
8181
await checkStatus();
82-
toast.success('License deactivated. You can now use it on another device.');
82+
toast.success('License deactivated successfully');
8383
} catch (error: any) {
8484
console.error('Failed to deactivate license:', error);
8585
toast.error(error || 'Failed to deactivate license');

src/lib/hotkey-conflicts.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Known system hotkey conflicts for different platforms
3+
* These hotkeys are reserved by the OS and may not work reliably
4+
*/
5+
6+
interface ConflictInfo {
7+
hotkey: string;
8+
description: string;
9+
severity: 'error' | 'warning';
10+
}
11+
12+
const WINDOWS_CONFLICTS: ConflictInfo[] = [
13+
// Windows system hotkeys
14+
{ hotkey: 'Ctrl+Shift+Period', description: 'May conflict with Windows IME or Office shortcuts', severity: 'warning' },
15+
{ hotkey: 'CommandOrControl+Shift+Period', description: 'May conflict with Windows IME or Office shortcuts', severity: 'warning' },
16+
{ hotkey: 'Win+Period', description: 'Windows Emoji Picker', severity: 'error' },
17+
{ hotkey: 'Win+Semicolon', description: 'Windows Emoji Picker', severity: 'error' },
18+
{ hotkey: 'Win+Space', description: 'Windows Language/Keyboard switcher', severity: 'error' },
19+
{ hotkey: 'Win+Tab', description: 'Windows Task View', severity: 'error' },
20+
{ hotkey: 'Alt+Tab', description: 'Windows App Switcher', severity: 'error' },
21+
{ hotkey: 'Ctrl+Alt+Delete', description: 'Windows Security Options', severity: 'error' },
22+
{ hotkey: 'Win+L', description: 'Windows Lock Screen', severity: 'error' },
23+
{ hotkey: 'Win+D', description: 'Windows Show Desktop', severity: 'error' },
24+
{ hotkey: 'Alt+F4', description: 'Windows Close Application', severity: 'warning' },
25+
{ hotkey: 'Ctrl+Shift+Escape', description: 'Windows Task Manager', severity: 'error' },
26+
];
27+
28+
const MACOS_CONFLICTS: ConflictInfo[] = [
29+
// macOS system hotkeys
30+
{ hotkey: 'CommandOrControl+Space', description: 'macOS Spotlight Search', severity: 'error' },
31+
{ hotkey: 'CommandOrControl+Tab', description: 'macOS App Switcher', severity: 'error' },
32+
{ hotkey: 'CommandOrControl+Shift+3', description: 'macOS Screenshot', severity: 'warning' },
33+
{ hotkey: 'CommandOrControl+Shift+4', description: 'macOS Screenshot Selection', severity: 'warning' },
34+
{ hotkey: 'CommandOrControl+Shift+5', description: 'macOS Screenshot/Recording', severity: 'warning' },
35+
{ hotkey: 'CommandOrControl+Option+Escape', description: 'macOS Force Quit', severity: 'error' },
36+
{ hotkey: 'CommandOrControl+Q', description: 'macOS Quit Application', severity: 'warning' },
37+
{ hotkey: 'CommandOrControl+W', description: 'macOS Close Window', severity: 'warning' },
38+
{ hotkey: 'CommandOrControl+M', description: 'macOS Minimize Window', severity: 'warning' },
39+
{ hotkey: 'CommandOrControl+H', description: 'macOS Hide Application', severity: 'warning' },
40+
{ hotkey: 'Control+CommandOrControl+Q', description: 'macOS Lock Screen', severity: 'error' },
41+
{ hotkey: 'Control+CommandOrControl+Space', description: 'macOS Emoji Picker', severity: 'warning' },
42+
];
43+
44+
const LINUX_CONFLICTS: ConflictInfo[] = [
45+
// Common Linux desktop environment hotkeys
46+
{ hotkey: 'Alt+Tab', description: 'Linux App Switcher', severity: 'error' },
47+
{ hotkey: 'Alt+F4', description: 'Linux Close Window', severity: 'warning' },
48+
{ hotkey: 'Super+Space', description: 'Linux Application Launcher (varies by DE)', severity: 'warning' },
49+
{ hotkey: 'Super+L', description: 'Linux Lock Screen (varies by DE)', severity: 'warning' },
50+
{ hotkey: 'Ctrl+Alt+T', description: 'Linux Terminal (varies by DE)', severity: 'warning' },
51+
{ hotkey: 'Ctrl+Alt+Delete', description: 'Linux System Monitor/Logout', severity: 'error' },
52+
{ hotkey: 'Ctrl+Alt+F1', description: 'Linux TTY1', severity: 'error' },
53+
{ hotkey: 'Ctrl+Alt+F2', description: 'Linux TTY2', severity: 'error' },
54+
];
55+
56+
/**
57+
* Check if a hotkey conflicts with known system shortcuts
58+
* @param hotkey The normalized hotkey string to check
59+
* @param platform Optional platform override (defaults to current platform)
60+
* @returns Conflict information if found, null otherwise
61+
*/
62+
export function checkForSystemConflict(
63+
hotkey: string,
64+
platform?: 'windows' | 'macos' | 'linux'
65+
): ConflictInfo | null {
66+
// Determine platform if not provided
67+
if (!platform) {
68+
if (typeof window !== 'undefined' && window.navigator) {
69+
const userAgent = window.navigator.userAgent.toLowerCase();
70+
if (userAgent.includes('win')) {
71+
platform = 'windows';
72+
} else if (userAgent.includes('mac')) {
73+
platform = 'macos';
74+
} else {
75+
platform = 'linux';
76+
}
77+
} else {
78+
// Default to windows if we can't detect
79+
platform = 'windows';
80+
}
81+
}
82+
83+
// Get the appropriate conflict list
84+
let conflicts: ConflictInfo[];
85+
switch (platform) {
86+
case 'macos':
87+
conflicts = MACOS_CONFLICTS;
88+
break;
89+
case 'linux':
90+
conflicts = LINUX_CONFLICTS;
91+
break;
92+
case 'windows':
93+
default:
94+
conflicts = WINDOWS_CONFLICTS;
95+
break;
96+
}
97+
98+
// Check for exact match (case-insensitive)
99+
const normalizedHotkey = hotkey.toLowerCase();
100+
const conflict = conflicts.find(c =>
101+
c.hotkey.toLowerCase() === normalizedHotkey
102+
);
103+
104+
return conflict || null;
105+
}
106+
107+
/**
108+
* Get all known conflicts for the current platform
109+
* @param platform Optional platform override
110+
* @returns Array of all known conflicts
111+
*/
112+
export function getAllConflicts(
113+
platform?: 'windows' | 'macos' | 'linux'
114+
): ConflictInfo[] {
115+
if (!platform) {
116+
if (typeof window !== 'undefined' && window.navigator) {
117+
const userAgent = window.navigator.userAgent.toLowerCase();
118+
if (userAgent.includes('win')) {
119+
platform = 'windows';
120+
} else if (userAgent.includes('mac')) {
121+
platform = 'macos';
122+
} else {
123+
platform = 'linux';
124+
}
125+
} else {
126+
platform = 'windows';
127+
}
128+
}
129+
130+
switch (platform) {
131+
case 'macos':
132+
return MACOS_CONFLICTS;
133+
case 'linux':
134+
return LINUX_CONFLICTS;
135+
case 'windows':
136+
default:
137+
return WINDOWS_CONFLICTS;
138+
}
139+
}
140+
141+
/**
142+
* Format a conflict warning message
143+
* @param conflict The conflict information
144+
* @returns Formatted warning message
145+
*/
146+
export function formatConflictMessage(conflict: ConflictInfo): string {
147+
if (conflict.severity === 'error') {
148+
return `⚠️ This hotkey is reserved by the system: ${conflict.description}`;
149+
} else {
150+
return `ℹ️ This hotkey may conflict: ${conflict.description}`;
151+
}
152+
}

0 commit comments

Comments
 (0)