cloud storage client
git clone https://git.lucas.co/cce-cloud.git
feat: switcher rows draw the window title without its (app_id)
The icon now says which app a window belongs to, so the suffix is noise.
Only the drawn and measured label drops it: the compositor maps the echoed
selection back to a window by the row's whole text, so the item text keeps
the suffix, which also leaves the app_id searchable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 7 ++++---
src/main.rs | 53 +++++++++++++++++++++++++++++++++++++----------------
2 files changed, 41 insertions(+), 19 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index ac611dd..8abdebb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -98,9 +98,10 @@ needs-stdin decision, in `run_client()`).
edge. Apps and the Super-Tab switcher are the only lists with icons: other
Dmenu/Path items are arbitrary strings with nothing to look up, and their
gutter stays 0. The switcher's rows are the compositor's `Title (app_id)`
- lines; `switcher_app_id` reads the id back out of the row (the row itself is
- left as sent, since the compositor maps the echoed text back to a window) and
- `desktop_icon_index` maps it to a `.desktop` entry's `Icon=` by file stem,
+ lines; `split_switcher_row` splits one into title and id. The row text itself
+ is left as sent, since the compositor maps the echoed text back to a window —
+ only the drawn and measured label drops the suffix (`row_label`), so typing
+ an app_id still filters. `desktop_icon_index` maps the id to a `.desktop` entry's `Icon=` by file stem,
`StartupWMClass`, or last reverse-DNS component, falling back to the app_id
itself, which is the name cce's own apps install their icons under. The rows
stream in over stdin, so `resolve_switcher_icons` runs on each ingest.
diff --git a/src/main.rs b/src/main.rs
index efa09c6..2202e67 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -408,16 +408,17 @@ fn scan_apps() -> Vec<AppInfo> {
apps
}
-/// The app_id in a Super-Tab switcher row. The compositor
+/// A Super-Tab switcher row split into `(title, app_id)`. The compositor
/// (`launch_window_switcher` in cce-compositor's window_manager.rs) writes each
-/// window as `Title (app_id)`, or the bare app_id when the title is empty, and
-/// maps the echoed row back to a window by its whole text — so the row is left
-/// as sent and the id is only read back out of it here. The LAST parenthesised
-/// group is the id: a title may carry parentheses of its own.
-fn switcher_app_id(item: &str) -> &str {
+/// window as `Title (app_id)`, or the bare app_id when the title is empty —
+/// which is then both halves. It maps the echoed row back to a window by its
+/// whole text, so the row itself is left as sent: the title is only what is
+/// drawn, and the id is only what the icon is looked up by. The LAST
+/// parenthesised group is the id: a title may carry parentheses of its own.
+fn split_switcher_row(item: &str) -> (&str, &str) {
item.strip_suffix(')')
- .and_then(|rest| rest.rfind(" (").map(|i| &rest[i + 2..]))
- .unwrap_or(item)
+ .and_then(|rest| rest.rfind(" (").map(|i| (&rest[..i], &rest[i + 2..])))
+ .unwrap_or((item, item))
}
/// app_id → `Icon=` value, from every `.desktop` file on the search path.
@@ -750,6 +751,10 @@ pub struct FuzzelWidget {
/// every row, not just the ones that resolved, so a list with one missing
/// icon keeps a straight text edge instead of ragging in and out.
icon_gutter: f32,
+ /// Rows are the window switcher's `Title (app_id)` lines: draw only the
+ /// title (see [`split_switcher_row`]). The item text — what filtering
+ /// matches and what a selection echoes back — keeps the suffix.
+ pub switcher_rows: bool,
/// Row under the pointer, by filtered index — the hover wash and the
/// brighter label. Distinct from `selected`: hovering never moves the
/// keyboard selection, only a click does.
@@ -821,6 +826,7 @@ impl FuzzelWidget {
selected: 0,
icons: std::collections::HashMap::new(),
icon_gutter: 0.0,
+ switcher_rows: false,
hovered: None,
cursor: None,
// Designer raise/sink treatment: the bar idles sunk under the
@@ -1019,6 +1025,16 @@ impl FuzzelWidget {
self.icons.contains_key(item)
}
+ /// What a row draws for `item` — the item itself, except for the window
+ /// switcher's rows (see `switcher_rows`).
+ pub fn row_label<'a>(&self, item: &'a str) -> &'a str {
+ if self.switcher_rows {
+ split_switcher_row(item).0
+ } else {
+ item
+ }
+ }
+
/// The square an icon is fitted into for the row drawn at `draw_y`.
fn icon_rect(&self, draw_y: f32, item_h: f32, w: u32, h: u32) -> cce_ui::scene::layout::Rect {
// Fit the longer side to ICON_PX so a non-square icon keeps its aspect
@@ -1720,6 +1736,7 @@ impl State {
let mut fuzzel = FuzzelWidget::new(prompt);
fuzzel.set_rect(0.0, 0.0, lw, lh);
+ fuzzel.switcher_rows = switcher_mode;
let stdin_state = Arc::new(Mutex::new(StdinState {
items: Vec::new(),
@@ -1963,7 +1980,7 @@ impl State {
let icons: Vec<(String, (u32, u32, u32))> = new
.into_iter()
.filter_map(|item| {
- let name = icon_name_for_app_id(index, switcher_app_id(item));
+ let name = icon_name_for_app_id(index, split_switcher_row(item).1);
let img = cce_ui::icon::upload_themed(&name, ICON_PX.ceil() as u32 * 2)?;
Some((item.clone(), img))
})
@@ -2021,7 +2038,8 @@ impl State {
}
} else {
for item in &self.fuzzel.filtered_items {
- let buf = make_text_buffer(&mut self.font_system, item, 13.0);
+ let label = self.fuzzel.row_label(item);
+ let buf = make_text_buffer(&mut self.font_system, label, 13.0);
let tw = buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0);
if tw > max_text_w {
max_text_w = tw;
@@ -4552,12 +4570,15 @@ mod tests {
}
#[test]
- fn switcher_rows_yield_their_app_id() {
- assert_eq!(switcher_app_id("~/src - Terminal (cce-terminal)"), "cce-terminal");
+ fn switcher_rows_split_into_title_and_app_id() {
+ assert_eq!(split_switcher_row("~/src - Terminal (cce-terminal)"), ("~/src - Terminal", "cce-terminal"));
// The last group is the id; the title's own parentheses are not.
- assert_eq!(switcher_app_id("Inbox (3) - Mail (org.gnome.Evolution)"), "org.gnome.Evolution");
- // An untitled window is sent as the bare app_id.
- assert_eq!(switcher_app_id("firefox"), "firefox");
+ assert_eq!(
+ split_switcher_row("Inbox (3) - Mail (org.gnome.Evolution)"),
+ ("Inbox (3) - Mail", "org.gnome.Evolution")
+ );
+ // An untitled window is sent as the bare app_id, which is both halves.
+ assert_eq!(split_switcher_row("firefox"), ("firefox", "firefox"));
}
#[test]
@@ -4891,7 +4912,7 @@ impl FuzzelWidget {
};
labels.push(TextLabel {
- text: item_text.clone(),
+ text: self.row_label(item_text).to_string(),
// Indented past the icon column whether or not THIS row
// resolved an icon — see `icon_gutter`.
x: self.text_x() + self.icon_gutter,