diff --git a/src/actor/reactor.rs b/src/actor/reactor.rs index 54dfc949..e5501970 100644 --- a/src/actor/reactor.rs +++ b/src/actor/reactor.rs @@ -293,6 +293,8 @@ pub enum ReactorCommand { selector: DisplaySelector, window_id: Option, }, + /// Toggle whether rift manages the current macOS space + ToggleSpaceActivated, } #[derive(Default, Debug, Clone)] @@ -860,6 +862,9 @@ impl Reactor { Event::Command(Command::Reactor(ReactorCommand::CloseWindow { window_server_id })) => { CommandEventHandler::handle_command_reactor_close_window(self, window_server_id) } + Event::Command(Command::Reactor(ReactorCommand::ToggleSpaceActivated)) => { + CommandEventHandler::handle_command_reactor_toggle_space_activated(self) + } _ => (), } if let Some(raised_window) = raised_window { diff --git a/src/actor/reactor/events/command.rs b/src/actor/reactor/events/command.rs index 86a26ca0..2ee3f53b 100644 --- a/src/actor/reactor/events/command.rs +++ b/src/actor/reactor/events/command.rs @@ -443,4 +443,14 @@ impl CommandEventHandler { warn!("Close window command ignored because no window is tracked"); } } + + pub fn handle_command_reactor_toggle_space_activated(reactor: &mut Reactor) { + if let Some(wm) = reactor.communication_manager.wm_sender.as_ref() { + let _ = wm.send(crate::actor::wm_controller::WmEvent::Command( + crate::actor::wm_controller::WmCommand::Wm( + crate::actor::wm_controller::WmCmd::ToggleSpaceActivated, + ), + )); + } + } } diff --git a/src/bin/rift-cli.rs b/src/bin/rift-cli.rs index b2c560ec..aa3bb32d 100644 --- a/src/bin/rift-cli.rs +++ b/src/bin/rift-cli.rs @@ -110,10 +110,19 @@ enum ExecuteCommands { #[command(subcommand)] display_cmd: DisplayCommands, }, + /// macOS space management commands + Space { + #[command(subcommand)] + space_cmd: SpaceCommands, + }, /// Save current state and exit rift SaveAndExit, /// Show timing metrics ShowTiming, + /// Print layout tree to stdout (for debugging) + Debug, + /// Print serialized engine state to stdout (for debugging) + Serialize, } #[derive(Subcommand)] @@ -122,9 +131,20 @@ enum WindowCommands { Next, /// Focus the previous window Prev, - /// Move focus in a direction + /// Focus a window by direction or by specific window ID + #[command(group = clap::ArgGroup::new("focus-target").required(true).multiple(false))] Focus { - direction: String, // up, down, left, right + /// Direction to move focus (up, down, left, right) + #[arg(long, group = "focus-target")] + direction: Option, + + /// Internal window ID in pid:idx format (e.g., "1234:0") + #[arg(long, group = "focus-target")] + window_id: Option, + + /// Optional window server ID for fallback focusing (only used with --window-id) + #[arg(long, requires = "window_id")] + window_server_id: Option, }, /// Toggle window floating state ToggleFloat, @@ -190,6 +210,17 @@ enum LayoutCommands { ToggleFocusFloat, } +#[derive(Subcommand)] +enum SpaceCommands { + /// Toggle whether rift manages the current macOS space + ToggleActivated, + /// Switch to an adjacent macOS space (Mission Control spaces, not virtual workspaces) + Switch { + /// Direction to switch (left, right, up, down) + direction: String, + }, +} + #[derive(Subcommand)] enum ConfigCommands { /// Update animation settings @@ -451,12 +482,19 @@ fn build_execute_request(execute: ExecuteCommands) -> Result map_display_command(display_cmd)?, + ExecuteCommands::Space { space_cmd } => map_space_command(space_cmd)?, ExecuteCommands::SaveAndExit => { RiftCommand::Reactor(reactor::Command::Reactor(reactor::ReactorCommand::SaveAndExit)) } ExecuteCommands::ShowTiming => RiftCommand::Reactor(reactor::Command::Metrics( rift_wm::common::log::MetricsCommand::ShowTiming, )), + ExecuteCommands::Debug => { + RiftCommand::Reactor(reactor::Command::Reactor(reactor::ReactorCommand::Debug)) + } + ExecuteCommands::Serialize => { + RiftCommand::Reactor(reactor::Command::Reactor(reactor::ReactorCommand::Serialize)) + } }; if let RiftCommand::Config(rift_wm::common::config::ConfigCommand::GetConfig) = &rift_command { @@ -492,9 +530,33 @@ fn map_window_command(cmd: WindowCommands) -> Result { match cmd { WindowCommands::Next => Ok(RiftCommand::Reactor(reactor::Command::Layout(LC::NextWindow))), WindowCommands::Prev => Ok(RiftCommand::Reactor(reactor::Command::Layout(LC::PrevWindow))), - WindowCommands::Focus { direction } => Ok(RiftCommand::Reactor(reactor::Command::Layout( - LC::MoveFocus(direction.into()), - ))), + WindowCommands::Focus { + direction, + window_id, + window_server_id, + } => { + // Handle direction-based focus + if let Some(dir) = direction { + return Ok(RiftCommand::Reactor(reactor::Command::Layout(LC::MoveFocus( + dir.into(), + )))); + } + + // Handle window-id-based focus + if let Some(wid_str) = window_id { + let wid = parse_window_id(&wid_str)?; + let wsid = window_server_id.map(WindowServerId::new); + return Ok(RiftCommand::Reactor(reactor::Command::Reactor( + reactor::ReactorCommand::FocusWindow { + window_id: wid, + window_server_id: wsid, + }, + ))); + } + + // This shouldn't happen due to clap's ArgGroup validation + Err("Focus command requires either --direction or --window-id".to_string()) + } WindowCommands::ToggleFloat => Ok(RiftCommand::Reactor(reactor::Command::Layout( LC::ToggleWindowFloating, ))), @@ -537,6 +599,22 @@ fn parse_window_server_id(input: &str) -> Result { Ok(WindowServerId::new(value)) } +fn parse_window_id(input: &str) -> Result { + let trimmed = input.trim(); + if trimmed.is_empty() { + return Err("window_id cannot be empty".to_string()); + } + + // Try parsing as JSON array format [pid, idx] using serde + let json_array = format!("[{}]", trimmed.replace(':', ",")); + serde_json::from_str(&json_array).map_err(|e| { + format!( + "Invalid window_id format '{}'. Expected 'pid:idx' (e.g., '1234:1'). Error: {}", + trimmed, e + ) + }) +} + fn map_workspace_command(cmd: WorkspaceCommands) -> Result { use layout::LayoutCommand as LC; match cmd { @@ -720,6 +798,20 @@ fn map_display_command(cmd: DisplayCommands) -> Result { } } +fn map_space_command(cmd: SpaceCommands) -> Result { + match cmd { + SpaceCommands::ToggleActivated => Ok(RiftCommand::Reactor(reactor::Command::Reactor( + reactor::ReactorCommand::ToggleSpaceActivated, + ))), + SpaceCommands::Switch { direction } => { + let dir = parse_focus_direction(&direction)?; + Ok(RiftCommand::Reactor(reactor::Command::Reactor( + reactor::ReactorCommand::SwitchSpace(dir), + ))) + } + } +} + fn build_display_selector( direction: Option, index: Option,