From 5e92d081c6a612df2f64563d422839d2beac3c8e Mon Sep 17 00:00:00 2001 From: wowitsjack Date: Sat, 27 Dec 2025 19:07:21 +1000 Subject: [PATCH] add type-to-select option for keyboard navigation --- i18n/en/cosmic_files.ftl | 1 + src/app.rs | 26 ++++++++++++++++++++++++++ src/config.rs | 1 + src/dialog.rs | 18 ++++++++++++++++++ src/tab.rs | 25 +++++++++++++++++++++++++ 5 files changed, 71 insertions(+) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index fc51354..96132a2 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -297,6 +297,7 @@ light = Light type-to-search = Type to search type-to-search-recursive = Searches the current folder and all subfolders type-to-search-enter-path = Enters the path to the directory or file +type-to-search-select = Selects the first matching file or folder # Context menu add-to-sidebar = Add to sidebar diff --git a/src/app.rs b/src/app.rs index ec9a11a..35a26cb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -733,6 +733,8 @@ pub struct App { windows: FxHashMap, nav_dnd_hover: Option<(Location, Instant)>, tab_dnd_hover: Option<(Entity, Instant)>, + type_select_prefix: String, + type_select_last_key: Option, nav_drag_id: DragId, tab_drag_id: DragId, auto_scroll_speed: Option, @@ -1985,6 +1987,12 @@ impl App { Some(self.config.type_to_search), Message::SetTypeToSearch, )) + .add(widget::radio( + widget::text::body(fl!("type-to-search-select")), + TypeToSearch::SelectByPrefix, + Some(self.config.type_to_search), + Message::SetTypeToSearch, + )) .into(), widget::settings::section() .title(fl!("other")) @@ -2213,6 +2221,8 @@ impl Application for App { windows: FxHashMap::default(), nav_dnd_hover: None, tab_dnd_hover: None, + type_select_prefix: String::new(), + type_select_last_key: None, nav_drag_id: DragId::new(), tab_drag_id: DragId::new(), auto_scroll_speed: None, @@ -3039,6 +3049,22 @@ impl Application for App { } } } + TypeToSearch::SelectByPrefix => { + // Reset buffer if timeout elapsed + if let Some(last_key) = self.type_select_last_key { + if last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT { + self.type_select_prefix.clear(); + } + } + + // Accumulate character and select + self.type_select_prefix.push_str(&text.to_lowercase()); + self.type_select_last_key = Some(Instant::now()); + + if let Some(tab) = self.tab_model.data_mut::(entity) { + tab.select_by_prefix(&self.type_select_prefix); + } + } } } } diff --git a/src/config.rs b/src/config.rs index d537a3f..7f606bd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -106,6 +106,7 @@ impl Favorite { pub enum TypeToSearch { Recursive, EnterPath, + SelectByPrefix, } #[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)] diff --git a/src/dialog.rs b/src/dialog.rs index b4dc047..90b4fd8 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -568,6 +568,8 @@ struct App { FxHashSet, )>, auto_scroll_speed: Option, + type_select_prefix: String, + type_select_last_key: Option, } impl App { @@ -1038,6 +1040,8 @@ impl Application for App { key_binds, watcher_opt: None, auto_scroll_speed: None, + type_select_prefix: String::new(), + type_select_last_key: None, }; let commands = Task::batch([ @@ -1448,6 +1452,20 @@ impl Application for App { Some(location.with_path(PathBuf::from(path_string)).into()); } } + TypeToSearch::SelectByPrefix => { + // Reset buffer if timeout elapsed + if let Some(last_key) = self.type_select_last_key { + if last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT { + self.type_select_prefix.clear(); + } + } + + // Accumulate character and select + self.type_select_prefix.push_str(&text.to_lowercase()); + self.type_select_last_key = Some(Instant::now()); + + self.tab.select_by_prefix(&self.type_select_prefix); + } } } } diff --git a/src/tab.rs b/src/tab.rs index c744608..75bbc97 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -98,6 +98,7 @@ use uzers::{get_group_by_gid, get_user_by_uid}; pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500); pub const HOVER_DURATION: Duration = Duration::from_millis(1600); +pub const TYPE_SELECT_TIMEOUT: Duration = Duration::from_millis(1000); //TODO: best limit for search items const MAX_SEARCH_LATENCY: Duration = Duration::from_millis(20); const MAX_SEARCH_RESULTS: usize = 200; @@ -2821,6 +2822,30 @@ impl Tab { } } + /// Selects the first item whose name starts with the given prefix (case-insensitive). + /// Returns true if an item was selected. + pub fn select_by_prefix(&mut self, prefix: &str) -> bool { + let prefix_lower = prefix.to_lowercase(); + self.select_focus = None; + + if let Some(ref mut items) = self.items_opt { + // First, deselect all items + for item in items.iter_mut() { + item.selected = false; + } + + // Find first matching item + for (i, item) in items.iter_mut().enumerate() { + if item.name.to_lowercase().starts_with(&prefix_lower) { + item.selected = true; + self.select_focus = Some(i); + return true; + } + } + } + false + } + pub fn select_paths(&mut self, paths: Vec) { self.select_focus = None; if let Some(ref mut items) = self.items_opt {