Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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(¢ers, 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()