git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit52db9d9a06677e24992414bc8de015cc3f327711
parent79fbd76a00
authorLucas Galante <[email protected]>
date2026-07-16 12:29
feat: dispatch directional focus actions; ccectl focus-up/down/left/right

The FocusUp/Down/Left/Right arm collects visible non-status windows
(same filter as FocusNext), computes virtual-surface centers from
virtual_x/y + box_geom * scale, and delegates the choice to the policy
crate's focus::directional_focus before focusing and raising the winner.
Defaults super+k/j/h/l come from the policy crate's DEFAULT_BINDINGS.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PXSsCppeDFmSRRM5NAug5a

 src/cce_ctl.rs               |  1 +
 src/server/window_manager.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 65 insertions(+)

diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index b37d27b..1871c23 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -37,6 +37,7 @@ fn usage(name: &str, to_stderr: bool) {
     print("  minimize");
     print("  focus-next");
     print("  focus-prev");
+    print("  focus-up | focus-down | focus-left | focus-right");
     print("  window-switcher            # open the alt-tab window switcher (cce-cloud overlay)");
     print("  fullscreen");
     print("  mode-next");
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 01a3a7d..77f9f40 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -1771,6 +1771,54 @@ impl WindowManager {
                     }
                 }
             }
+            Action::FocusUp | Action::FocusDown | Action::FocusLeft | Action::FocusRight => {
+                if let Some(seat) = self.first_seat() {
+                    let focused_win = if let crate::seat::Focus::Window(fw) = (*seat).focused {
+                        fw
+                    } else {
+                        std::ptr::null_mut()
+                    };
+
+                    let render_list = &mut self.rendering_requested.list as *mut ffi::wl_list as *mut WlList;
+                    let mut curr = (*render_list).next;
+                    let mut visible_windows = Vec::new();
+                    while curr != render_list {
+                        let next = (*curr).next;
+                        let node = crate::container_of!(curr, crate::wm_node::WmNode, link);
+                        if let crate::wm_node::WmNodeType::Window(window) = (*node).get() {
+                            if !window.is_null() && !(*window).closed && !(*window).minimized {
+                                let is_status_bar = (*window).get_app_id_string()
+                                    .map_or(false, |aid| aid.starts_with("cce-status"));
+                                if !is_status_bar {
+                                    visible_windows.push(window);
+                                }
+                            }
+                        }
+                        curr = next;
+                    }
+
+                    // Window centers on the virtual surface — the policy
+                    // crate picks the winner, the seat applies it.
+                    let centers: Vec<(f64, f64)> = visible_windows
+                        .iter()
+                        .map(|&w| {
+                            (
+                                (*w).virtual_x + (*w).box_geom.width as f64 * (*w).scale / 2.0,
+                                (*w).virtual_y + (*w).box_geom.height as f64 * (*w).scale / 2.0,
+                            )
+                        })
+                        .collect();
+                    let focused_idx = visible_windows.iter().position(|&w| w == focused_win);
+                    let dir = crate::policy::focus::Direction::from_action(*action)
+                        .expect("arm only matches directional focus actions");
+                    if let Some(target_idx) = crate::policy::focus::directional_focus(&centers, focused_idx, dir) {
+                        let target_win = visible_windows[target_idx];
+                        (*seat).focus(crate::seat::Focus::Window(target_win));
+                        self.raise_window(target_win);
+                        self.dirty_windowing();
+                    }
+                }
+            }
             Action::WindowSwitcher => {
                 self.launch_window_switcher();
             }
@@ -2440,6 +2488,22 @@ impl WindowManager {
                 self.execute_action(&crate::config::Action::FocusPrev, None);
                 "ok\n".to_string()
             }
+            "focus-up" => {
+                self.execute_action(&crate::config::Action::FocusUp, None);
+                "ok\n".to_string()
+            }
+            "focus-down" => {
+                self.execute_action(&crate::config::Action::FocusDown, None);
+                "ok\n".to_string()
+            }
+            "focus-left" => {
+                self.execute_action(&crate::config::Action::FocusLeft, None);
+                "ok\n".to_string()
+            }
+            "focus-right" => {
+                self.execute_action(&crate::config::Action::FocusRight, None);
+                "ok\n".to_string()
+            }
             "window-switcher" => {
                 self.execute_action(&crate::config::Action::WindowSwitcher, None);
                 "ok\n".to_string()