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
6 changes: 6 additions & 0 deletions rift.default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ fade_enabled = false
# native macos mission control fade is about 180ms
fade_duration_ms = 180.0

[settings.ui.command_switcher]
enabled = true

# Trackpad gestures
[settings.gestures]
# Enable horizontal swipes to switch virtual workspaces
Expand Down Expand Up @@ -320,6 +323,9 @@ comb1 = "Alt + Shift"

"Alt + Tab" = "switch_to_last_workspace"

"Ctrl + Right" = { switcher = "all_windows" }
"Ctrl + Alt + Tab" = { switcher = "current_workspace" }

"Alt + Shift + Left" = { join_window = "left" }
"Alt + Shift + Right" = { join_window = "right" }
"Alt + Shift + Up" = { join_window = "up" }
Expand Down
1 change: 1 addition & 0 deletions src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use tracing::Span;

pub mod app;
pub mod broadcast;
pub mod command_switcher;
pub mod config;
pub mod config_watcher;
pub mod drag_swap;
Expand Down
221 changes: 221 additions & 0 deletions src/actor/command_switcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use std::rc::Rc;
use std::time::Duration;

use r#continue::continuation;
use objc2_app_kit::NSScreen;
use objc2_core_foundation::{CGPoint, CGRect, CGSize};
use objc2_foundation::MainThreadMarker;
use tracing::{instrument, warn};

use crate::actor::{self, reactor};
use crate::common::config::{CommandSwitcherDisplayMode, CommandSwitcherSettings, Config};
use crate::model::server::{WindowData, WorkspaceData, WorkspaceQueryResponse};
use crate::sys::dispatch::block_on;
use crate::sys::screen;
use crate::ui::command_switcher::{
CommandSwitcherAction, CommandSwitcherMode, CommandSwitcherOverlay,
};

#[derive(Debug)]
pub enum Event {
Show(CommandSwitcherDisplayMode),
Dismiss,
UpdateConfig(Config),
}

pub type Sender = actor::Sender<Event>;
pub type Receiver = actor::Receiver<Event>;

pub struct CommandSwitcherActor {
config: Config,
settings: CommandSwitcherSettings,
rx: Receiver,
reactor_tx: reactor::Sender,
overlay: Option<CommandSwitcherOverlay>,
mtm: MainThreadMarker,
active: bool,
last_mode: Option<CommandSwitcherDisplayMode>,
}

impl CommandSwitcherActor {
pub fn new(
config: Config,
rx: Receiver,
reactor_tx: reactor::Sender,
mtm: MainThreadMarker,
) -> Self {
let settings = config.settings.ui.command_switcher.clone();
Self {
config,
settings,
rx,
reactor_tx,
overlay: None,
mtm,
active: false,
last_mode: None,
}
}

pub async fn run(mut self) {
while let Some((span, event)) = self.rx.recv().await {
let _guard = span.enter();
self.handle_event(event);
}
}

#[instrument(skip(self))]
fn handle_event(&mut self, event: Event) {
match event {
Event::UpdateConfig(config) => self.apply_config(config),
Event::Dismiss => self.hide_overlay(),
Event::Show(mode) => {
if self.settings.enabled {
let _ = self.show_contents(mode);
}
}
}
}

fn apply_config(&mut self, config: Config) {
self.config = config.clone();
self.settings = config.settings.ui.command_switcher.clone();
if !self.settings.enabled {
self.hide_overlay();
self.overlay = None;
} else if self.active {
if let Some(mode) = self.last_mode {
let _ = self.show_contents(mode);
}
}
}

fn show_contents(&mut self, mode: CommandSwitcherDisplayMode) -> bool {
let Some(payload) = self.fetch_mode_data(mode) else {
self.hide_overlay();
return false;
};
let Some(overlay) = self.ensure_overlay() else {
return false;
};
overlay.update(payload);
self.active = true;
self.last_mode = Some(mode);
true
}

fn ensure_overlay(&mut self) -> Option<&CommandSwitcherOverlay> {
if self.overlay.is_none() {
let (frame, scale) = if let Some(screen) = NSScreen::mainScreen(self.mtm) {
(screen.frame(), screen.backingScaleFactor())
} else {
(
CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(1280.0, 800.0)),
1.0,
)
};
let overlay = CommandSwitcherOverlay::new(self.config.clone(), self.mtm, frame, scale);
let self_ptr: *mut CommandSwitcherActor = self;
overlay.set_action_handler(Rc::new(move |action| unsafe {
let this: &mut CommandSwitcherActor = &mut *self_ptr;
this.handle_overlay_action(action);
}));
self.overlay = Some(overlay);
}
self.overlay.as_ref()
}

fn fetch_mode_data(&mut self, mode: CommandSwitcherDisplayMode) -> Option<CommandSwitcherMode> {
match mode {
CommandSwitcherDisplayMode::CurrentWorkspace => {
let active_space = screen::get_active_space_number();
let (tx, fut) = continuation::<Vec<WindowData>>();
let _ = self.reactor_tx.try_send(reactor::Event::QueryWindows {
space_id: active_space,
response: tx,
});
match block_on(fut, Duration::from_millis(750)) {
Ok(windows) => Some(CommandSwitcherMode::CurrentWorkspace(windows)),
Err(_) => {
warn!("command switcher: windows query timed out");
None
}
}
}
CommandSwitcherDisplayMode::AllWindows => {
let (tx, fut) = continuation::<WorkspaceQueryResponse>();
let _ = self.reactor_tx.try_send(reactor::Event::QueryWorkspaces(tx));
match block_on(fut, Duration::from_millis(750)) {
Ok(resp) => {
Some(CommandSwitcherMode::AllWindows(flatten_windows(resp.workspaces)))
}
Err(_) => {
warn!("command switcher: workspace query timed out");
None
}
}
}
CommandSwitcherDisplayMode::Workspaces => {
let (tx, fut) = continuation::<WorkspaceQueryResponse>();
let _ = self.reactor_tx.try_send(reactor::Event::QueryWorkspaces(tx));
match block_on(fut, Duration::from_millis(750)) {
Ok(resp) => Some(CommandSwitcherMode::Workspaces(filter_workspaces(
resp.workspaces,
))),
Err(_) => {
warn!("command switcher: workspace query timed out");
None
}
}
}
}
}

fn handle_overlay_action(&mut self, action: CommandSwitcherAction) {
match action {
CommandSwitcherAction::Dismiss => self.hide_overlay(),
CommandSwitcherAction::SwitchToWorkspace(index) => {
let _ =
self.reactor_tx.try_send(reactor::Event::Command(reactor::Command::Layout(
crate::layout_engine::LayoutCommand::SwitchToWorkspace(index),
)));
self.hide_overlay();
}
CommandSwitcherAction::FocusWindow { window_id, window_server_id } => {
let _ =
self.reactor_tx.try_send(reactor::Event::Command(reactor::Command::Reactor(
reactor::ReactorCommand::FocusWindow { window_id, window_server_id },
)));
self.hide_overlay();
}
}
}

fn hide_overlay(&mut self) {
if let Some(overlay) = self.overlay.as_ref() {
overlay.hide();
}
self.active = false;
}
}

fn flatten_windows(workspaces: Vec<WorkspaceData>) -> Vec<WindowData> {
let mut active = Vec::new();
let mut others = Vec::new();
for mut workspace in workspaces {
if workspace.is_active {
active.append(&mut workspace.windows);
} else {
others.append(&mut workspace.windows);
}
}
active.extend(others);
active
}

fn filter_workspaces(workspaces: Vec<WorkspaceData>) -> Vec<WorkspaceData> {
workspaces
.into_iter()
.filter(|ws| ws.is_active || ws.is_last_active || !ws.windows.is_empty())
.collect()
}
12 changes: 11 additions & 1 deletion src/actor/reactor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use crate::actor::raise_manager::{self, RaiseManager, RaiseRequest};
use crate::actor::reactor::events::window_discovery::WindowDiscoveryHandler;
use crate::actor::{self, menu_bar, stack_line};
use crate::common::collections::{BTreeMap, HashMap, HashSet};
use crate::common::config::Config;
use crate::common::config::{CommandSwitcherDisplayMode, Config};
use crate::common::log::MetricsCommand;
use crate::layout_engine::{self as layout, Direction, LayoutCommand, LayoutEngine, LayoutEvent};
use crate::model::VirtualWorkspaceId;
Expand Down Expand Up @@ -261,6 +261,10 @@ pub enum ReactorCommand {
ShowMissionControlAll,
ShowMissionControlCurrent,
DismissMissionControl,
ShowCommandSwitcher {
mode: CommandSwitcherDisplayMode,
},
CommandSwitcherDismiss,
}

#[derive(Default, Debug, Clone)]
Expand Down Expand Up @@ -788,6 +792,12 @@ impl Reactor {
Event::Command(Command::Reactor(ReactorCommand::DismissMissionControl)) => {
CommandEventHandler::handle_command_reactor_dismiss_mission_control(self);
}
Event::Command(Command::Reactor(ReactorCommand::ShowCommandSwitcher { mode })) => {
CommandEventHandler::handle_command_reactor_show_command_switcher(self, mode);
}
Event::Command(Command::Reactor(ReactorCommand::CommandSwitcherDismiss)) => {
CommandEventHandler::handle_command_reactor_command_switcher_dismiss(self);
}
_ => (),
}
if let Some(raised_window) = raised_window {
Expand Down
25 changes: 24 additions & 1 deletion src/actor/reactor/events/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::actor::reactor::{Reactor, WorkspaceSwitchState};
use crate::actor::stack_line::Event as StackLineEvent;
use crate::actor::wm_controller::WmEvent;
use crate::common::collections::HashMap;
use crate::common::config::{self as config, Config};
use crate::common::config::{self as config, CommandSwitcherDisplayMode, Config};
use crate::common::log::{MetricsCommand, handle_command};
use crate::layout_engine::{EventResponse, LayoutCommand, LayoutEvent};
use crate::sys::screen::{SpaceId, order_visible_spaces_by_position};
Expand Down Expand Up @@ -221,4 +221,27 @@ impl CommandEventHandler {
reactor.set_mission_control_active(false);
}
}

pub fn handle_command_reactor_show_command_switcher(
reactor: &mut Reactor,
mode: CommandSwitcherDisplayMode,
) {
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::Switcher(mode),
),
));
}
}

pub fn handle_command_reactor_command_switcher_dismiss(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::CommandSwitcherDismiss,
),
));
}
}
}
10 changes: 10 additions & 0 deletions src/actor/reactor/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ impl Reactor {
Vec::new()
};

let last_workspace_id = space_id.and_then(|space| {
self.layout_manager
.layout_engine
.virtual_workspace_manager()
.last_workspace(space)
});

for (index, (workspace_id, workspace_name)) in workspace_list.iter().enumerate() {
let is_active = if let Some(space) = space_id {
self.layout_manager.layout_engine.active_workspace(space) == Some(*workspace_id)
Expand Down Expand Up @@ -169,10 +176,13 @@ impl Reactor {
}
}

let is_last_active = last_workspace_id == Some(*workspace_id);

workspaces.push(WorkspaceData {
id: format!("{:?}", workspace_id),
name: workspace_name.to_string(),
is_active,
is_last_active,
window_count: windows.len(),
windows,
index,
Expand Down
Loading
Loading