Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/actor/reactor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ pub enum ReactorCommand {
selector: DisplaySelector,
window_id: Option<u32>,
},
/// Toggle whether rift manages the current macOS space
ToggleSpaceActivated,
}

#[derive(Default, Debug, Clone)]
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions src/actor/reactor/events/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
));
}
}
}
102 changes: 97 additions & 5 deletions src/bin/rift-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<String>,

/// Internal window ID in pid:idx format (e.g., "1234:0")
#[arg(long, group = "focus-target")]
window_id: Option<String>,

/// Optional window server ID for fallback focusing (only used with --window-id)
#[arg(long, requires = "window_id")]
window_server_id: Option<u32>,
},
/// Toggle window floating state
ToggleFloat,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -451,12 +482,19 @@ fn build_execute_request(execute: ExecuteCommands) -> Result<RiftRequest, String
map_mission_control_command(mission_cmd)?
}
ExecuteCommands::Display { display_cmd } => 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 {
Expand Down Expand Up @@ -492,9 +530,33 @@ fn map_window_command(cmd: WindowCommands) -> Result<RiftCommand, String> {
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,
))),
Expand Down Expand Up @@ -537,6 +599,22 @@ fn parse_window_server_id(input: &str) -> Result<WindowServerId, String> {
Ok(WindowServerId::new(value))
}

fn parse_window_id(input: &str) -> Result<rift_wm::actor::app::WindowId, String> {
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<RiftCommand, String> {
use layout::LayoutCommand as LC;
match cmd {
Expand Down Expand Up @@ -720,6 +798,20 @@ fn map_display_command(cmd: DisplayCommands) -> Result<RiftCommand, String> {
}
}

fn map_space_command(cmd: SpaceCommands) -> Result<RiftCommand, String> {
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<String>,
index: Option<usize>,
Expand Down
Loading