Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lang/en/LC_MESSAGES/meatshell.po
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,6 @@ msgstr "Copied"

msgid "Clear sort"
msgstr "Clear sort"

msgid "Search by name or host..."
msgstr "Search by name or host..."
3 changes: 3 additions & 0 deletions lang/zh/LC_MESSAGES/meatshell.po
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,6 @@ msgstr "已复制"

msgid "Clear sort"
msgstr "清除排序"

msgid "Search by name or host..."
msgstr "按名称或主机搜索..."
67 changes: 50 additions & 17 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
{
Expand Down Expand Up @@ -2434,27 +2447,47 @@ fn jump_candidates(
}

fn sync_sessions_to_model(store: &ConfigStore, model: &VecModel<SessionInfo>) {
// 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<SessionInfo>, 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<String> = 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<String> = 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();

Expand Down Expand Up @@ -2482,9 +2515,9 @@ fn sync_sessions_to_model(store: &ConfigStore, model: &VecModel<SessionInfo>) {
let mut rows: Vec<SessionInfo> = 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());

Expand Down
8 changes: 8 additions & 0 deletions ui/app.slint
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ export component AppWindow inherits Window {

// --- Session list ------------------------------------------------------
in property <[SessionInfo]> sessions;
in-out property <string> host-search-query; // search filter for sessions
callback host-search-changed(string /* query */);

// --- Tabs --------------------------------------------------------------
in property <[TabInfo]> tabs;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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); }
Expand Down
2 changes: 2 additions & 0 deletions ui/terminal_view.slint
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
47 changes: 46 additions & 1 deletion ui/welcome.slint
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -226,6 +226,7 @@ component CollapseButton inherits Rectangle {
export component Welcome inherits Rectangle {
in property <[SessionInfo]> sessions;
in property <string> import-hint; // result text after an import
in-out property <string> search-query; // host search filter text
callback new-session();
callback import-ssh-config();
callback connect-session(string);
Expand All @@ -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 <bool> compact;
in property <string> dock-edge: "left";
in property <bool> lang-en: false; // UI language for placeholder text
property <bool> 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();
Expand Down Expand Up @@ -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;
Expand Down