From b1aab87f007c662641b97c3a22b01ec854454bd5 Mon Sep 17 00:00:00 2001 From: Maddisen Date: Sat, 11 Jul 2026 15:34:20 -0400 Subject: [PATCH 1/2] add hotkey recording support for the 2 settings fields --- src/app.rs | 33 +++- src/app/pages/settings.rs | 101 +++++++++--- src/app/tile.rs | 75 +++++---- src/app/tile/elm.rs | 9 +- src/app/tile/update.rs | 124 +++++++++++++-- src/platform/macos/launching.rs | 264 +++++++++++++++++++++++++++++++- 6 files changed, 539 insertions(+), 67 deletions(-) diff --git a/src/app.rs b/src/app.rs index aaafdca..45bc3a2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -49,6 +49,27 @@ pub enum SettingsTab { Commands, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HotkeyTarget { + Toggle, + Clipboard, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HotkeyCapture { + Idle, + Recording { + target: HotkeyTarget, + candidate: Option, + }, +} + +impl HotkeyCapture { + pub fn is_recording(&self) -> bool { + matches!(self, Self::Recording { .. }) + } +} + /// Actions that open a native file dialog #[derive(Debug, Clone)] pub enum FileDialogAction { @@ -169,14 +190,22 @@ pub enum Message { SimulatePaste(i32), OpenSettingsWindow, SettingsWindowOpened(window::Id), + KeyboardEvent { + event: iced::Event, + window: window::Id, + }, + BeginHotkeyCapture(HotkeyTarget), + HotkeyCaptureKeyPressed { + physical_key: iced::keyboard::key::Physical, + modifiers: iced::keyboard::Modifiers, + }, + FinishHotkeyCapture, } #[derive(Debug, Clone)] #[allow(unused)] pub enum SetConfigFields { ToDefault, - ToggleHotkey(String), - ClipboardHotkey(String), SetPosition(Position), PlaceHolder(String), SearchUrl(String), diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index 2c8739c..398b884 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -31,11 +31,12 @@ use crate::app::FileDialogAction; use crate::app::ResetField; use crate::app::SetConfigBufferFields; use crate::app::SetConfigThemeFields; -use crate::app::SettingsTab; +use crate::app::{HotkeyCapture, HotkeyTarget, SettingsTab}; use crate::commands::Function; use crate::config::MainPage; use crate::config::Shelly; use crate::config::ThemeMode; +use crate::platform::macos::launching::Shortcut; use crate::styles::delete_button_style; use crate::styles::settings_add_button_style; use crate::styles::settings_radio_button_style; @@ -53,7 +54,11 @@ const SETTINGS_ITEM_HEIGHT: u32 = 55; const SETTINGS_ITEM_COL_SPACING: u32 = 3; const SETTINGS_INPUT_WIDTH: f32 = 250.0; -pub fn settings_page(config: Config, settings_tab: SettingsTab) -> Element<'static, Message> { +pub fn settings_page( + config: Config, + settings_tab: SettingsTab, + hotkey_capture: HotkeyCapture, +) -> Element<'static, Message> { let config = Box::new(config.clone()); let theme = config.theme.clone(); @@ -83,7 +88,7 @@ pub fn settings_page(config: Config, settings_tab: SettingsTab) -> Element<'stat .align_x(Alignment::Center); let tab_content: Column<'static, Message> = match settings_tab { - SettingsTab::General => general_tab(config.clone(), theme.clone()), + SettingsTab::General => general_tab(config.clone(), theme.clone(), hotkey_capture), SettingsTab::Appearance => appearance_tab(config.clone(), theme.clone()), SettingsTab::Commands => commands_tab(config.clone(), theme.clone()), }; @@ -166,44 +171,44 @@ fn reset_button(theme: crate::config::Theme, field: ResetField) -> Element<'stat .into() } -fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'static, Message> { - let theme_clone = theme.clone(); +fn general_tab( + config: Box, + theme: crate::config::Theme, + hotkey_capture: HotkeyCapture, +) -> Column<'static, Message> { let hotkey = settings_row_with_reset( settings_item_row([ settings_hint_text( theme.clone(), "Toggle hotkey", - Some("Use \"+\" as a seperator"), + Some("Click the field to record and ESC once done"), ), Space::new().width(Length::Fill).into(), - text_input("Toggle Hotkey", &config.toggle_hotkey) - .on_input(|input| Message::SetConfig(SetConfigFields::ToggleHotkey(input.clone()))) - .on_submit(Message::WriteConfig) - .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) - .style(move |_, _| settings_text_input_item_style(&theme_clone)) - .into(), + hotkey_field( + &config.toggle_hotkey, + HotkeyTarget::Toggle, + &hotkey_capture, + theme.clone(), + ), ]), ResetField::ToggleHotkey, theme.clone(), ); - let theme_clone = theme.clone(); let cb_hotkey = settings_row_with_reset( settings_item_row([ settings_hint_text( theme.clone(), "Clipboard hotkey", - Some("Use \"+\" as a seperator"), + Some("Click the field to record and ESC once done"), ), Space::new().width(Length::Fill).into(), - text_input("Clipboard Hotkey", &config.clipboard_hotkey) - .on_input(|input| { - Message::SetConfig(SetConfigFields::ClipboardHotkey(input.clone())) - }) - .on_submit(Message::WriteConfig) - .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) - .style(move |_, _| settings_text_input_item_style(&theme_clone)) - .into(), + hotkey_field( + &config.clipboard_hotkey, + HotkeyTarget::Clipboard, + &hotkey_capture, + theme.clone(), + ), ]), ResetField::ClipboardHotkey, theme.clone(), @@ -478,6 +483,58 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat .spacing(10) } +fn hotkey_field( + value: &str, + target: HotkeyTarget, + capture: &HotkeyCapture, + theme: crate::config::Theme, +) -> Element<'static, Message> { + let (label, recording) = match capture { + HotkeyCapture::Recording { + target: active_target, + candidate, + } if *active_target == target => ( + candidate + .as_ref() + .map(Shortcut::display_string) + .unwrap_or_else(|| "Press a shortcut…".to_string()), + true, + ), + _ => ( + Shortcut::parse(value) + .map(|shortcut| shortcut.display_string()) + .unwrap_or_else(|_| value.to_string()), + false, + ), + }; + let theme_clone = theme.clone(); + + Button::new( + Text::new(label) + .font(theme.font()) + .align_y(Alignment::Center) + .width(Length::Fill), + ) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) + .height(36) + .padding([0, 12]) + .style(move |_, _| button::Style { + text_color: theme_clone.text_color(1.0), + background: Some(Background::Color(with_alpha( + tint(theme_clone.bg_color(), if recording { 0.16 } else { 0.08 }), + 0.9, + ))), + border: Border { + color: theme_clone.text_color(if recording { 0.8 } else { 0.3 }), + width: if recording { 1.0 } else { 0.2 }, + radius: Radius::new(10), + }, + ..Default::default() + }) + .on_press(Message::BeginHotkeyCapture(target)) + .into() +} + fn appearance_tab(config: Box, theme: crate::config::Theme) -> Column<'static, Message> { let theme_clone = theme.clone(); let theme_mode_setting = settings_row_without_reset(settings_item_row([ diff --git a/src/app/tile.rs b/src/app/tile.rs index 117edc8..2c4051d 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -3,7 +3,7 @@ pub mod elm; pub mod update; use crate::app::apps::App; -use crate::app::{ArrowKey, Message, Move, Page}; +use crate::app::{ArrowKey, HotkeyCapture, Message, Move, Page}; use crate::autoupdate::new_version_available; use crate::clipboard::ClipBoardContentType; use crate::config::{Config, Shelly}; @@ -205,6 +205,7 @@ pub struct Tile { pub settings_tab: crate::app::SettingsTab, debouncer: Debouncer, pub settings_window: Option, + pub hotkey_capture: HotkeyCapture, previous_input_source: Option, } @@ -278,34 +279,34 @@ impl Tile { /// - Keypresses (escape to close the window) /// - Window focus changes pub fn subscription(&self) -> Subscription { - let keyboard = event::listen_with(|event, _, id| match event { - iced::Event::Keyboard(keyboard::Event::KeyPressed { - key: keyboard::Key::Named(keyboard::key::Named::Escape), - .. - }) => Some(Message::EscKeyPressed(id)), - iced::Event::Keyboard(keyboard::Event::KeyPressed { - key: keyboard::Key::Character(cha), - modifiers: Modifiers::LOGO, - .. - }) => { - if cha.to_string() == "," { - return Some(Message::OpenSettingsWindow); - } - None - } - _ => None, - }); - Subscription::batch([ - Subscription::run(handle_hot_reloading), - keyboard, - Subscription::run(crate::platform::macos::urlscheme::url_stream), - Subscription::run(handle_recipient), - Subscription::run(reload_events), - Subscription::run(handle_version_and_rankings), - Subscription::run(handle_theme_mode), - Subscription::run(handle_clipboard_history), - Subscription::run(handle_file_search), - window::close_events().map(Message::HideWindow), + let is_recording_hotkey = self.hotkey_capture.is_recording(); + let capture_events = if is_recording_hotkey { + event::listen_with(|event, _, window| match event { + iced::Event::Keyboard(_) => Some(Message::KeyboardEvent { event, window }), + _ => None, + }) + } else { + Subscription::none() + }; + let window_keyboard = if is_recording_hotkey { + Subscription::none() + } else { + event::listen_with(|event, _, id| match event { + iced::Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(keyboard::key::Named::Escape), + .. + }) => Some(Message::EscKeyPressed(id)), + iced::Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Character(cha), + modifiers: Modifiers::LOGO, + .. + }) if cha == "," => Some(Message::OpenSettingsWindow), + _ => None, + }) + }; + let keyboard = if is_recording_hotkey { + Subscription::none() + } else { keyboard::listen().filter_map(|event| { if let keyboard::Event::KeyPressed { key, modifiers, .. } = event { match key { @@ -347,7 +348,21 @@ impl Tile { } else { None } - }), + }) + }; + Subscription::batch([ + Subscription::run(handle_hot_reloading), + capture_events, + window_keyboard, + Subscription::run(crate::platform::macos::urlscheme::url_stream), + Subscription::run(handle_recipient), + Subscription::run(reload_events), + Subscription::run(handle_version_and_rankings), + Subscription::run(handle_theme_mode), + Subscription::run(handle_clipboard_history), + Subscription::run(handle_file_search), + window::close_events().map(Message::HideWindow), + keyboard, window::events() .with(self.focused) .filter_map(|(focused, (wid, event))| match event { diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 5f1e2c7..7326be9 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -19,7 +19,7 @@ use rayon::slice::ParallelSliceMut; use crate::app::pages::emoji::emoji_page; use crate::app::pages::settings::settings_page; use crate::app::tile::{AppIndex, Hotkeys}; -use crate::app::{DEFAULT_WINDOW_HEIGHT, SettingsTab, ToApp, ToApps}; +use crate::app::{DEFAULT_WINDOW_HEIGHT, HotkeyCapture, SettingsTab, ToApp, ToApps}; use crate::config::Theme; use crate::debounce::Debouncer; use crate::platform; @@ -106,6 +106,7 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) { settings_tab: SettingsTab::General, debouncer: Debouncer::new(config.debounce_delay), settings_window: None, + hotkey_capture: HotkeyCapture::Idle, previous_input_source: None, }, Task::batch([open.map(|_| Message::OpenWindow)]), @@ -115,7 +116,11 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) { /// The elm View function that renders the entire rustcast window pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { if tile.settings_window == Some(wid) { - return settings_page(tile.config.clone(), tile.settings_tab); + return settings_page( + tile.config.clone(), + tile.settings_tab, + tile.hotkey_capture.clone(), + ); }; if tile.visible { diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 175fca5..015f5c7 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -36,7 +36,7 @@ use crate::app::menubar::menu_builder; use crate::app::menubar::menu_icon; use crate::app::settings_window_settings; use crate::app::tile::AppIndex; -use crate::app::{Message, Page, tile::Tile}; +use crate::app::{HotkeyCapture, Message, Page, tile::Tile}; use crate::autoupdate::download_latest_app; use crate::calculator::Expr; use crate::commands::Function; @@ -110,6 +110,30 @@ fn message_for_open_command(command: &AppCommand) -> Message { } } +fn refresh_global_handler(tile: &mut Tile) { + let Some(sender) = tile.sender.clone() else { + return; + }; + + tile.hotkeys.handle = None; + match global_handler(sender, tile.hotkeys.all_hotkeys()) { + Ok(handle) => tile.hotkeys.handle = Some(handle), + Err(error) => { + log::error!("Error when registering hotkey: {error}"); + std::process::exit(1); + } + } +} + +fn is_available_hotkey(tile: &Tile, target: crate::app::HotkeyTarget, shortcut: &Shortcut) -> bool { + let other_builtin = match target { + crate::app::HotkeyTarget::Toggle => &tile.hotkeys.clipboard_hotkey, + crate::app::HotkeyTarget::Clipboard => &tile.hotkeys.toggle, + }; + + shortcut != other_builtin && !tile.hotkeys.shells.contains_key(shortcut) +} + /// Handle the "elm" update pub fn handle_update(tile: &mut Tile, message: Message) -> Task { match message { @@ -200,13 +224,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Message::SetSender(sender) => { tile.sender = Some(sender.clone()); - match global_handler(sender.clone(), tile.hotkeys.all_hotkeys()) { - Ok(a) => tile.hotkeys.handle = Some(a), - Err(e) => { - log::error!("Error when registering hotkey: {e}"); - std::process::exit(1); - } - }; + refresh_global_handler(tile); if tile.config.show_trayicon { tile.tray_icon = Some(menu_icon(tile.config.clone(), sender)); } @@ -561,7 +579,12 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { return Task::none(); } if tile.settings_window == Some(a) { + let was_recording = tile.hotkey_capture.is_recording(); tile.settings_window = None; + tile.hotkey_capture = HotkeyCapture::Idle; + if was_recording { + refresh_global_handler(tile); + } return Task::none(); } info!("Hiding RustCast window"); @@ -832,8 +855,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Message::SetConfig(config) => { let mut final_config = tile.config.clone(); match config.clone() { - SetConfigFields::ToggleHotkey(hk) => final_config.toggle_hotkey = hk, - SetConfigFields::ClipboardHotkey(hk) => final_config.clipboard_hotkey = hk, SetConfigFields::ClipboardHistory(cbhist) => final_config.cbhist = cbhist, SetConfigFields::Modes(Editable::Create((key, value))) => { final_config.modes.insert(key, value); @@ -990,6 +1011,10 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Message::ResetField(field) => { let default = Config::default(); + let reset_hotkey = matches!( + field, + ResetField::ToggleHotkey | ResetField::ClipboardHotkey + ); match field { ResetField::ToggleHotkey => tile.config.toggle_hotkey = default.toggle_hotkey, ResetField::ClipboardHotkey => { @@ -1036,6 +1061,15 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tile.config.cbhist_paste_on_select = default.cbhist_paste_on_select } } + if reset_hotkey { + if let Ok(shortcut) = Shortcut::parse(&tile.config.toggle_hotkey) { + tile.hotkeys.toggle = shortcut; + } + if let Ok(shortcut) = Shortcut::parse(&tile.config.clipboard_hotkey) { + tile.hotkeys.clipboard_hotkey = shortcut; + } + refresh_global_handler(tile); + } tile.theme = tile.config.theme.clone().into(); Task::none() } @@ -1100,6 +1134,75 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } } Message::SettingsWindowOpened(_id) => Task::none(), + Message::KeyboardEvent { event, window } => { + if tile.hotkey_capture.is_recording() && tile.settings_window == Some(window) { + return match event { + iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { + key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape), + .. + }) => Task::done(Message::FinishHotkeyCapture), + iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { + physical_key, + modifiers, + repeat: false, + .. + }) => Task::done(Message::HotkeyCaptureKeyPressed { + physical_key, + modifiers, + }), + _ => Task::none(), + }; + } + Task::none() + } + Message::BeginHotkeyCapture(target) => { + tile.hotkey_capture = HotkeyCapture::Recording { + target, + candidate: None, + }; + tile.hotkeys.handle = None; + Task::none() + } + Message::HotkeyCaptureKeyPressed { + physical_key, + modifiers, + } => { + if let HotkeyCapture::Recording { candidate, .. } = &mut tile.hotkey_capture + && let Some(shortcut) = Shortcut::from_iced(physical_key, modifiers) + { + *candidate = Some(shortcut); + } + Task::none() + } + Message::FinishHotkeyCapture => { + let captured = if let HotkeyCapture::Recording { + target, + candidate: Some(shortcut), + } = &tile.hotkey_capture + { + Some((*target, shortcut.clone())) + } else { + None + }; + + if let Some((target, shortcut)) = captured + && is_available_hotkey(tile, target, &shortcut) + { + match target { + crate::app::HotkeyTarget::Toggle => { + tile.config.toggle_hotkey = shortcut.to_config_string(); + tile.hotkeys.toggle = shortcut; + } + crate::app::HotkeyTarget::Clipboard => { + tile.config.clipboard_hotkey = shortcut.to_config_string(); + tile.hotkeys.clipboard_hotkey = shortcut; + } + } + } + tile.hotkey_capture = HotkeyCapture::Idle; + refresh_global_handler(tile); + Task::none() + } } } @@ -1456,6 +1559,7 @@ mod tests { settings_tab: crate::app::SettingsTab::General, debouncer: crate::debounce::Debouncer::new(10), settings_window: None, + hotkey_capture: HotkeyCapture::Idle, previous_input_source: None, } } diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs index 68e1a6e..1c69b20 100644 --- a/src/platform/macos/launching.rs +++ b/src/platform/macos/launching.rs @@ -17,6 +17,10 @@ use crate::{ app::{Message, tile::ExtSender}, platform::macos::accessibility::ensure_accessibility_permission, }; +use iced::keyboard::{ + Modifiers, + key::{Code, Physical}, +}; #[derive(Clone, Debug)] pub struct EventTapHandle { @@ -231,6 +235,242 @@ impl Shortcut { if has_mods { Some(mods) } else { None }, )) } + + pub fn from_iced(physical_key: Physical, modifiers: Modifiers) -> Option { + let Physical::Code(code) = physical_key else { + return None; + }; + + let key_code = match code { + Code::KeyA => 0x00, + Code::KeyS => 0x01, + Code::KeyD => 0x02, + Code::KeyF => 0x03, + Code::KeyH => 0x04, + Code::KeyG => 0x05, + Code::KeyZ => 0x06, + Code::KeyX => 0x07, + Code::KeyC => 0x08, + Code::KeyV => 0x09, + Code::KeyB => 0x0b, + Code::KeyQ => 0x0c, + Code::KeyW => 0x0d, + Code::KeyE => 0x0e, + Code::KeyR => 0x0f, + Code::KeyY => 0x10, + Code::KeyT => 0x11, + Code::KeyO => 0x1f, + Code::KeyU => 0x20, + Code::KeyI => 0x22, + Code::KeyP => 0x23, + Code::KeyL => 0x25, + Code::KeyJ => 0x26, + Code::KeyK => 0x28, + Code::KeyN => 0x2d, + Code::KeyM => 0x2e, + Code::Digit1 => 0x12, + Code::Digit2 => 0x13, + Code::Digit3 => 0x14, + Code::Digit4 => 0x15, + Code::Digit5 => 0x17, + Code::Digit6 => 0x16, + Code::Digit7 => 0x1a, + Code::Digit8 => 0x1c, + Code::Digit9 => 0x19, + Code::Digit0 => 0x1d, + Code::Enter => 0x24, + Code::Tab => 0x30, + Code::Space => 0x31, + Code::Backspace => 0x33, + Code::Escape => 0x35, + Code::ArrowLeft => 0x7b, + Code::ArrowRight => 0x7c, + Code::ArrowDown => 0x7d, + Code::ArrowUp => 0x7e, + Code::Home => 0x73, + Code::End => 0x77, + Code::PageUp => 0x74, + Code::PageDown => 0x79, + Code::F1 => 0x7a, + Code::F2 => 0x78, + Code::F3 => 0x63, + Code::F4 => 0x76, + Code::F5 => 0x60, + Code::F6 => 0x61, + Code::F7 => 0x62, + Code::F8 => 0x64, + Code::F9 => 0x65, + Code::F10 => 0x6d, + Code::F11 => 0x67, + Code::F12 => 0x6f, + Code::Minus => 0x1b, + Code::Equal => 0x18, + Code::BracketLeft => 0x21, + Code::BracketRight => 0x1e, + Code::Backslash => 0x2a, + Code::Semicolon => 0x29, + Code::Quote => 0x27, + Code::Backquote => 0x32, + Code::Comma => 0x2b, + Code::Period => 0x2f, + Code::Slash => 0x2c, + _ => return None, + }; + + let mut mods = NSEventModifierFlags::empty(); + if modifiers.logo() { + mods |= NSEventModifierFlags::Command; + } + if modifiers.alt() { + mods |= NSEventModifierFlags::Option; + } + if modifiers.control() { + mods |= NSEventModifierFlags::Control; + } + if modifiers.shift() { + mods |= NSEventModifierFlags::Shift; + } + if mods.is_empty() { + return None; + } + + Some(Self::new(Some(key_code), Some(mods.0))) + } + + pub fn to_config_string(&self) -> String { + let mut parts = Vec::new(); + let mods = self.mods.unwrap_or_default(); + if mods & NSEventModifierFlags::Command.0 != 0 { + parts.push("cmd".to_string()); + } + if mods & NSEventModifierFlags::Option.0 != 0 { + parts.push("option".to_string()); + } + if mods & NSEventModifierFlags::Control.0 != 0 { + parts.push("ctrl".to_string()); + } + if mods & NSEventModifierFlags::Shift.0 != 0 { + parts.push("shift".to_string()); + } + if let Some(key_code) = self.key_code.and_then(keycode_to_name) { + parts.push(key_code.to_string()); + } + parts.join("+") + } + + pub fn display_string(&self) -> String { + let mut display = String::new(); + let mods = self.mods.unwrap_or_default(); + if mods & NSEventModifierFlags::Command.0 != 0 { + display.push('⌘'); + } + if mods & NSEventModifierFlags::Option.0 != 0 { + display.push('⌥'); + } + if mods & NSEventModifierFlags::Control.0 != 0 { + display.push('⌃'); + } + if mods & NSEventModifierFlags::Shift.0 != 0 { + display.push('⇧'); + } + if let Some(key) = self.key_code.and_then(keycode_to_name) { + let key = match key { + "return" => "↩", + "space" => "Space", + "delete" => "⌫", + "escape" => "⎋", + "left" => "←", + "right" => "→", + "down" => "↓", + "up" => "↑", + _ => key, + }; + if key.len() == 1 && key.as_bytes()[0].is_ascii_alphabetic() { + display.push_str(&key.to_ascii_uppercase()); + } else { + display.push_str(key); + } + } + display + } +} + +fn keycode_to_name(code: u16) -> Option<&'static str> { + Some(match code { + 0x00 => "a", + 0x01 => "s", + 0x02 => "d", + 0x03 => "f", + 0x04 => "h", + 0x05 => "g", + 0x06 => "z", + 0x07 => "x", + 0x08 => "c", + 0x09 => "v", + 0x0b => "b", + 0x0c => "q", + 0x0d => "w", + 0x0e => "e", + 0x0f => "r", + 0x10 => "y", + 0x11 => "t", + 0x1f => "o", + 0x20 => "u", + 0x22 => "i", + 0x23 => "p", + 0x25 => "l", + 0x26 => "j", + 0x28 => "k", + 0x2d => "n", + 0x2e => "m", + 0x12 => "1", + 0x13 => "2", + 0x14 => "3", + 0x15 => "4", + 0x17 => "5", + 0x16 => "6", + 0x1a => "7", + 0x1c => "8", + 0x19 => "9", + 0x1d => "0", + 0x24 => "return", + 0x30 => "tab", + 0x31 => "space", + 0x33 => "delete", + 0x35 => "escape", + 0x7b => "left", + 0x7c => "right", + 0x7d => "down", + 0x7e => "up", + 0x73 => "home", + 0x77 => "end", + 0x74 => "pageup", + 0x79 => "pagedown", + 0x7a => "f1", + 0x78 => "f2", + 0x63 => "f3", + 0x76 => "f4", + 0x60 => "f5", + 0x61 => "f6", + 0x62 => "f7", + 0x64 => "f8", + 0x65 => "f9", + 0x6d => "f10", + 0x67 => "f11", + 0x6f => "f12", + 0x1b => "-", + 0x18 => "=", + 0x21 => "[", + 0x1e => "]", + 0x2a => "\\", + 0x29 => ";", + 0x27 => "'", + 0x32 => "`", + 0x2b => ",", + 0x2f => ".", + 0x2c => "/", + _ => return None, + }) } fn str_to_keycode(s: &str) -> Result { @@ -316,9 +556,31 @@ fn str_to_keycode(s: &str) -> Result { "," | "comma" => 0x2b, "." | "period" => 0x2f, "/" | "slash" => 0x2c, - _ => return Err(format!("Unknown key: '{}'", s)), }; Ok(code) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn recorded_shortcut_uses_config_format() -> Result<(), String> { + let shortcut = Shortcut::from_iced( + Physical::Code(Code::KeyK), + Modifiers::LOGO | Modifiers::SHIFT, + ) + .ok_or_else(|| "modified shortcut should be recorded".to_string())?; + + assert_eq!(shortcut.to_config_string(), "cmd+shift+k"); + assert_eq!(shortcut.display_string(), "⌘⇧K"); + Ok(()) + } + + #[test] + fn recorded_shortcut_requires_a_modifier() { + assert!(Shortcut::from_iced(Physical::Code(Code::KeyK), Modifiers::NONE).is_none()); + } +} From 72b8629267df73e888ad1a285934e7ba3132d481 Mon Sep 17 00:00:00 2001 From: Maddisen Date: Sat, 11 Jul 2026 15:36:22 -0400 Subject: [PATCH 2/2] remove reset fields no longer used in settings --- src/app.rs | 12 ------------ src/app/tile/update.rs | 27 --------------------------- 2 files changed, 39 deletions(-) diff --git a/src/app.rs b/src/app.rs index 45bc3a2..1c9e2eb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -86,22 +86,10 @@ pub enum ResetField { Placeholder, SearchUrl, DebounceDelay, - StartAtLogin, - AutoUpdate, - HapticFeedback, - ShowMenubarIcon, - ClipboardHistory, - ClipboardPasteOnSelect, - MainPage, - ShowScrollbar, - ClearOnHide, - ClearOnEnter, - ShowIcons, Font, EventDuration, TextColor, BackgroundColor, - ThemeMode, Aliases, Modes, SearchDirs, diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 015f5c7..a6989b7 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -1023,30 +1023,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ResetField::Placeholder => tile.config.placeholder = default.placeholder, ResetField::SearchUrl => tile.config.search_url = default.search_url, ResetField::DebounceDelay => tile.config.debounce_delay = default.debounce_delay, - ResetField::StartAtLogin => tile.config.start_at_login = default.start_at_login, - ResetField::AutoUpdate => tile.config.auto_update = default.auto_update, - ResetField::HapticFeedback => tile.config.haptic_feedback = default.haptic_feedback, - ResetField::ShowMenubarIcon => tile.config.show_trayicon = default.show_trayicon, - ResetField::ClipboardHistory => tile.config.cbhist = default.cbhist, - ResetField::MainPage => tile.config.main_page = default.main_page, - ResetField::ThemeMode => { - tile.config.theme.theme_mode = default.theme.theme_mode; - let is_dark = crate::platform::macos::is_dark_mode(); - let (text, bg, secondary) = default.theme.theme_mode.presets(is_dark); - tile.config.theme.text_color = text; - tile.config.theme.background_color = bg; - tile.config.theme.secondary_bg_color = secondary; - } - ResetField::ShowScrollbar => { - tile.config.theme.show_scroll_bar = default.theme.show_scroll_bar - } - ResetField::ClearOnHide => { - tile.config.buffer_rules.clear_on_hide = default.buffer_rules.clear_on_hide - } - ResetField::ClearOnEnter => { - tile.config.buffer_rules.clear_on_enter = default.buffer_rules.clear_on_enter - } - ResetField::ShowIcons => tile.config.theme.show_icons = default.theme.show_icons, ResetField::Font => tile.config.theme.font = default.theme.font, ResetField::EventDuration => tile.config.event_duration = default.event_duration, ResetField::TextColor => tile.config.theme.text_color = default.theme.text_color, @@ -1057,9 +1033,6 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ResetField::Modes => tile.config.modes = default.modes, ResetField::SearchDirs => tile.config.search_dirs = default.search_dirs, ResetField::ShellCommands => tile.config.shells = default.shells, - ResetField::ClipboardPasteOnSelect => { - tile.config.cbhist_paste_on_select = default.cbhist_paste_on_select - } } if reset_hotkey { if let Ok(shortcut) = Shortcut::parse(&tile.config.toggle_hotkey) {