git.lucas.co / cce-cloud
cloud storage client
git clone https://git.lucas.co/cce-cloud.git

commit22dda7010b22475c94b83df2fa4db3eb02fb451f
parent30e54da114
authorLucas Galante <[email protected]>
date2026-06-24 21:19
feat: implement switcher mode with Tab cycling, super hold-release selection, and single-click dmenu activate

 src/main.rs | 125 +++++++++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 94 insertions(+), 31 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index f2dee83..fdfa94c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -732,6 +732,7 @@ struct AppInfo {
 struct StdinState {
     items: Vec<String>,
     new_data: bool,
+    cycle_next: usize,
 }
 
 
@@ -778,6 +779,7 @@ struct State {
     max_width: u32,
     max_height: u32,
     select_item: Option<String>,
+    switcher_mode: bool,
     last_tick: std::time::Instant,
     ui_context: cce_ui::context::UiContext,
     root_window: cce_ui::widget::Window,
@@ -800,6 +802,7 @@ impl State {
         align_right: bool,
         scale: f64,
         select_item: Option<String>,
+        switcher_mode: bool,
         json_layout_config: Option<JsonLayoutConfig>,
     ) -> (Self, Option<cce_ui::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1>) {
         cce_ui::scale::set_scale_factor(scale as f32);
@@ -1024,6 +1027,7 @@ impl State {
         let stdin_state = Arc::new(Mutex::new(StdinState {
             items: Vec::new(),
             new_data: false,
+            cycle_next: 0,
         }));
 
         let mut apps = Vec::new();
@@ -1034,8 +1038,13 @@ impl State {
                 for line in stdin.lock().lines() {
                     if let Ok(line) = line {
                         if let Ok(mut lock_state) = stdin_state_clone.lock() {
-                            lock_state.items.push(line);
-                            lock_state.new_data = true;
+                            if line == "__cce_switcher_next__" {
+                                lock_state.cycle_next += 1;
+                                lock_state.new_data = true;
+                            } else {
+                                lock_state.items.push(line);
+                                lock_state.new_data = true;
+                            }
                         }
                         let _ = stdin_sender.send(());
                     }
@@ -1108,6 +1117,7 @@ impl State {
             max_width: width,
             max_height: height,
             select_item,
+            switcher_mode,
             last_tick: std::time::Instant::now(),
             ui_context: cce_ui::context::UiContext::new(),
             root_window,
@@ -1124,23 +1134,38 @@ impl State {
         if let Ok(mut lock) = self.stdin_state.lock() {
             if lock.new_data {
                 lock.new_data = false;
+
+                let cycles = lock.cycle_next;
+                lock.cycle_next = 0;
+
+                let mut changed = false;
                 let items = lock.items.clone();
-                eprintln!("[cce-cloud debug] check_stdin_updates: items={:?}, select_item={:?}, currently selected={}", items, self.select_item, self.fuzzel.selected);
-                self.fuzzel.set_items(items);
-                if let Some(ref select_name) = self.select_item {
-                    let select_lower = select_name.to_lowercase();
-                    if let Some(idx) = self.fuzzel.filtered_items.iter().position(|item| item.to_lowercase() == select_lower) {
-                        eprintln!("[cce-cloud debug] Found match for select_item {:?} at index {}, setting selected", select_name, idx);
-                        self.fuzzel.selected = idx;
+                if self.fuzzel.all_items != items {
+                    self.fuzzel.set_items(items);
+                    changed = true;
+                    if let Some(ref select_name) = self.select_item {
+                        let select_lower = select_name.to_lowercase();
+                        if let Some(idx) = self.fuzzel.filtered_items.iter().position(|item| item.to_lowercase() == select_lower) {
+                            self.fuzzel.selected = idx;
+                            self.fuzzel.update_scroll();
+                            self.fuzzel.snap_to_selected();
+                            self.select_item = None;
+                        }
+                    } else if self.switcher_mode && self.fuzzel.filtered_items.len() > 1 {
+                        self.fuzzel.selected = 1;
                         self.fuzzel.update_scroll();
                         self.fuzzel.snap_to_selected();
-                        self.select_item = None;
-                    } else {
-                        eprintln!("[cce-cloud debug] No match found for select_item {:?} in filtered_items {:?}", select_name, self.fuzzel.filtered_items);
                     }
                 }
-                eprintln!("[cce-cloud debug] check_stdin_updates done: selected={}", self.fuzzel.selected);
-                return true;
+
+                if cycles > 0 && !self.fuzzel.filtered_items.is_empty() {
+                    self.fuzzel.selected = (self.fuzzel.selected + cycles) % self.fuzzel.filtered_items.len();
+                    self.fuzzel.update_scroll();
+                    self.fuzzel.snap_to_selected();
+                    changed = true;
+                }
+
+                return changed;
             }
         }
         false
@@ -1456,6 +1481,8 @@ struct AppState {
     exit: bool,
     redraw: bool,
     ctrl_pressed: bool,
+    super_pressed: bool,
+    switcher_mode: bool,
     fade_out: bool,
     fade_start: Option<std::time::Instant>,
     fade_factor: f32,
@@ -1470,6 +1497,34 @@ impl AppState {
             self.redraw = true;
         }
     }
+
+    fn trigger_select_and_close(&mut self) {
+        let mut should_close = false;
+        if let Some(st) = &mut self.state {
+            if !st.fuzzel.filtered_items.is_empty() {
+                if let Some(item) = st.fuzzel.filtered_items.get(st.fuzzel.selected) {
+                    println!("{}", item);
+                    match st.mode {
+                        LauncherMode::Apps => {
+                            if let Some(app) = st.apps.iter().find(|app| &app.name == item) {
+                                record_app_launch(&app.name);
+                                spawn_command(&app.exec);
+                            }
+                        }
+                        LauncherMode::Path => {
+                            spawn_command(item);
+                        }
+                        LauncherMode::Dmenu => {}
+                        LauncherMode::Json => {}
+                    }
+                    should_close = true;
+                }
+            }
+        }
+        if should_close {
+            self.trigger_close();
+        }
+    }
 }
 
 impl CompositorHandler for AppState {
@@ -1667,7 +1722,7 @@ impl PointerHandler for AppState {
                                     &mut st.ui_context,
                                 );
                                 if changed {
-                                    if st.fuzzel.selected == prev_selected {
+                                    if st.fuzzel.selected == prev_selected || st.switcher_mode || st.mode == LauncherMode::Dmenu {
                                         if let Some(item) = st.fuzzel.filtered_items.get(st.fuzzel.selected) {
                                             println!("{}", item);
                                             match st.mode {
@@ -1830,7 +1885,14 @@ impl KeyboardHandler for AppState {
         modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
         _layout: u32,
     ) {
+        let prev_super = self.super_pressed;
         self.ctrl_pressed = modifiers.ctrl;
+        self.super_pressed = modifiers.logo;
+
+        if self.switcher_mode && prev_super && !self.super_pressed {
+            eprintln!("[clear-cloud] Super modifier released in switcher mode, selecting currently highlighted item");
+            self.trigger_select_and_close();
+        }
     }
 }
 
@@ -2048,22 +2110,15 @@ impl AppState {
                         should_close = true;
                     }
                     Key::Named(NamedKey::Enter) => {
-                        if let Some(item) = st.fuzzel.filtered_items.get(st.fuzzel.selected) {
-                            println!("{}", item);
-                            match st.mode {
-                                LauncherMode::Apps => {
-                                    if let Some(app) = st.apps.iter().find(|app| &app.name == item) {
-                                        record_app_launch(&app.name);
-                                        spawn_command(&app.exec);
-                                    }
-                                }
-                                LauncherMode::Path => {
-                                    spawn_command(item);
-                                }
-                                LauncherMode::Dmenu => {}
-                                LauncherMode::Json => {}
-                            }
-                            should_close = true;
+                        self.trigger_select_and_close();
+                    }
+                    Key::Named(NamedKey::Tab) => {
+                        if !st.fuzzel.filtered_items.is_empty() {
+                            st.fuzzel.selected = (st.fuzzel.selected + 1) % st.fuzzel.filtered_items.len();
+                            st.fuzzel.update_scroll();
+                            st.fuzzel.snap_to_selected();
+                            st.upload_vertices();
+                            self.redraw = true;
                         }
                     }
                     Key::Named(NamedKey::ArrowDown) => {
@@ -2127,6 +2182,7 @@ fn main() {
     let mut y_pos: Option<i32> = None;
     let mut select_item: Option<String> = None;
     let mut align_right = false;
+    let mut switcher_mode = false;
 
     let args = std::env::args().skip(1).collect::<Vec<String>>();
     let mut i = 0;
@@ -2192,6 +2248,10 @@ fn main() {
         } else if arg == "--align-right" {
             align_right = true;
             i += 1;
+        } else if arg == "--switcher" {
+            switcher_mode = true;
+            mode = LauncherMode::Dmenu;
+            i += 1;
         } else {
             i += 1;
         }
@@ -2251,6 +2311,8 @@ fn main() {
         exit: false,
         redraw: false,
         ctrl_pressed: false,
+        super_pressed: false,
+        switcher_mode,
         fade_out: false,
         fade_start: None,
         fade_factor: 1.0,
@@ -2282,6 +2344,7 @@ fn main() {
         align_right,
         scale,
         select_item,
+        switcher_mode,
         json_layout_config,
     ));