diff --git a/lang/en/LC_MESSAGES/meatshell.po b/lang/en/LC_MESSAGES/meatshell.po index 6d0070fa..41252480 100644 --- a/lang/en/LC_MESSAGES/meatshell.po +++ b/lang/en/LC_MESSAGES/meatshell.po @@ -497,3 +497,6 @@ msgstr "Copied" msgid "Clear sort" msgstr "Clear sort" + +msgid "Search by name or host..." +msgstr "Search by name or host..." diff --git a/lang/zh/LC_MESSAGES/meatshell.po b/lang/zh/LC_MESSAGES/meatshell.po index c900e74a..7a454e12 100644 --- a/lang/zh/LC_MESSAGES/meatshell.po +++ b/lang/zh/LC_MESSAGES/meatshell.po @@ -497,3 +497,6 @@ msgstr "已复制" msgid "Clear sort" msgstr "清除排序" + +msgid "Search by name or host..." +msgstr "按名称或主机搜索..." diff --git a/src/app.rs b/src/app.rs index 383732b5..f62e5acc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1265,6 +1265,19 @@ pub fn run() -> Result<()> { sftp_follow_cd.clone(), ); + // Host search filter: re-sync the session list whenever the query changes. + { + let weak = window.as_weak(); + let store = store.clone(); + let sessions_model = sessions_model.clone(); + window.on_host_search_changed(move |query| { + let Some(w) = weak.upgrade() else { return }; + let q = query.to_string(); + w.set_host_search_query(q.clone().into()); + sync_sessions_to_model_with_filter(&store.borrow(), &sessions_model, &q); + }); + } + // Recompute the sidebar whenever the active tab changes (fired from Slint's // `changed active-tab-id`). { @@ -2434,27 +2447,47 @@ fn jump_candidates( } fn sync_sessions_to_model(store: &ConfigStore, model: &VecModel) { - // Group sessions by their `group` (named groups alphabetically, ungrouped - // last), then by name within each group, and tag the first row of every - // group with a header so the welcome list can render a folder heading (#41). + sync_sessions_to_model_with_filter(store, model, ""); +} + +/// Like `sync_sessions_to_model` but filters sessions by name or host (case-insensitive). +fn sync_sessions_to_model_with_filter(store: &ConfigStore, model: &VecModel, filter: &str) { let sessions = store.sessions(); + let filter_lower = filter.to_lowercase(); + let matches = |s: &Session| { + if filter.is_empty() { + return true; + } + s.name.to_lowercase().contains(&filter_lower) + || s.host.to_lowercase().contains(&filter_lower) + }; // Ordered list of display groups: // - "default" only when there are ungrouped sessions (group == "") // - named groups: explicit folders (incl. empty ones) ∪ sessions' groups, // de-duplicated, alphabetical. - let has_default = sessions.iter().any(|s| s.group.is_empty()); - let mut named: Vec = store - .groups() - .iter() - .cloned() - .chain( - sessions - .iter() - .filter(|s| !s.group.is_empty()) - .map(|s| s.group.clone()), - ) - .collect(); + // When a search filter is active, only include groups that contain at + // least one matching session, so empty groups don't clutter the results. + let has_default = sessions.iter().any(|s| s.group.is_empty() && matches(s)); + let mut named: Vec = if filter.is_empty() { + store + .groups() + .iter() + .cloned() + .chain( + sessions + .iter() + .filter(|s| !s.group.is_empty()) + .map(|s| s.group.clone()), + ) + .collect() + } else { + sessions + .iter() + .filter(|s| !s.group.is_empty() && matches(s)) + .map(|s| s.group.clone()) + .collect() + }; named.sort_by_key(|g| g.to_lowercase()); named.dedup(); @@ -2482,9 +2515,9 @@ fn sync_sessions_to_model(store: &ConfigStore, model: &VecModel) { let mut rows: Vec = Vec::new(); for group in &display_groups { let mut gs: Vec<&Session> = if group == "default" { - sessions.iter().filter(|s| s.group.is_empty()).collect() + sessions.iter().filter(|s| s.group.is_empty() && matches(s)).collect() } else { - sessions.iter().filter(|s| &s.group == group).collect() + sessions.iter().filter(|s| &s.group == group && matches(s)).collect() }; gs.sort_by_key(|s| s.name.to_lowercase()); diff --git a/ui/app.slint b/ui/app.slint index 3bb5de66..80a66251 100644 --- a/ui/app.slint +++ b/ui/app.slint @@ -559,6 +559,8 @@ export component AppWindow inherits Window { // --- Session list ------------------------------------------------------ in property <[SessionInfo]> sessions; + in-out property host-search-query; // search filter for sessions + callback host-search-changed(string /* query */); // --- Tabs -------------------------------------------------------------- in property <[TabInfo]> tabs; @@ -896,6 +898,9 @@ export component AppWindow inherits Window { dock-edge: root.welcome-sidebar-dock; sessions: root.sessions; import-hint: root.ssh-import-hint; + search-query <=> root.host-search-query; + lang-en: root.lang-en; + search-changed(q) => { root.host-search-changed(q); } collapse => { root.welcome-collapsed = true; root.set-welcome-collapsed(true); @@ -1236,6 +1241,9 @@ export component AppWindow inherits Window { height: parent.height; sessions: root.sessions; import-hint: root.ssh-import-hint; + search-query <=> root.host-search-query; + lang-en: root.lang-en; + search-changed(q) => { root.host-search-changed(q); } new-session => { root.new-session-clicked(); } import-ssh-config => { root.import-ssh-config(); } connect-session(id) => { root.connect-session(id); } diff --git a/ui/terminal_view.slint b/ui/terminal_view.slint index fc07b18a..8a0ff653 100644 --- a/ui/terminal_view.slint +++ b/ui/terminal_view.slint @@ -1039,10 +1039,12 @@ export component TerminalView inherits Rectangle { } if cmd-input.text == "" : Text { x: 8px; + width: parent.width - 16px; height: parent.height; text: @tr("Type a command, Enter to send"); color: Theme.text-muted; font-size: Theme.fs-md; + horizontal-alignment: left; vertical-alignment: center; } } diff --git a/ui/welcome.slint b/ui/welcome.slint index 15aa5bdf..7a5ddb9b 100644 --- a/ui/welcome.slint +++ b/ui/welcome.slint @@ -1,6 +1,6 @@ import { Theme } from "theme.slint"; import { PrimaryButton, GhostButton } from "widgets.slint"; -import { ScrollView } from "std-widgets.slint"; +import { ScrollView, LineEdit } from "std-widgets.slint"; export struct SessionInfo { id: string, @@ -226,6 +226,7 @@ component CollapseButton inherits Rectangle { export component Welcome inherits Rectangle { in property <[SessionInfo]> sessions; in property import-hint; // result text after an import + in-out property search-query; // host search filter text callback new-session(); callback import-ssh-config(); callback connect-session(string); @@ -237,10 +238,12 @@ export component Welcome inherits Rectangle { callback edit-group(string /* current-name */); callback delete-group(string /* name */); callback remove-session(string); + callback search-changed(string /* query */); // Compact mode (welcome-as-sidebar): drop the big title/tagline header and // tighten padding so the session list fits a narrow docked panel (#v0.5). in property compact; in property dock-edge: "left"; + in property lang-en: false; // UI language for placeholder text property dock-horizontal: root.dock-edge == "left" || root.dock-edge == "right"; // Collapse the sidebar to the edge icon strip (compact mode only) (#v0.5 M5). callback collapse(); @@ -339,6 +342,48 @@ export component Welcome inherits Rectangle { clicked => { root.new-session(); } } + // Search box — filter sessions by name or host (#search-host). + Rectangle { + height: 30px; + border-radius: Theme.radius-sm; + border-width: 1px; + border-color: search-input.has-focus ? Theme.accent : Theme.border-subtle; + background: Theme.bg-root; + HorizontalLayout { + padding-left: 8px; + padding-right: 8px; + padding-top: 4px; + padding-bottom: 4px; + spacing: 6px; + Text { + text: "\u{E8B6}"; // Material Icons "search" + font-family: "Material Icons"; + color: Theme.text-muted; + font-size: Theme.fs-sm; + vertical-alignment: center; + } + Rectangle { + horizontal-stretch: 1; + background: transparent; + search-input := TextInput { + text <=> root.search-query; + font-family: Theme.ui-font-family; + font-size: Theme.fs-sm; + color: Theme.text-primary; + edited => { root.search-changed(root.search-query); } + } + } + if root.search-query == "" : Text { + text: @tr("Search by name or host..."); + font-family: Theme.ui-font-family; + font-size: Theme.fs-sm; + color: Theme.text-muted; + vertical-alignment: center; + } + + } + } + if sessions.length == 0 : Rectangle { vertical-stretch: 1; background: Theme.bg-root;