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
11 changes: 11 additions & 0 deletions cosmic-comp-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ pub struct CosmicCompConfig {
/// Hide the cursor after this many seconds of pointer inactivity (None disables)
pub cursor_hide_timeout: Option<u32>,
pub activation_policy: ActivationPolicy,
/// Rules mapping an application id to the workspace (1-based index) new
/// windows of that app should open on, regardless of the active workspace.
pub workspace_assignments: Vec<WorkspaceAssignment>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct WorkspaceAssignment {
pub app_id: String,
/// 1-based workspace number, matching what the workspaces UI shows.
pub workspace: u32,
}

impl Default for CosmicCompConfig {
Expand Down Expand Up @@ -140,6 +150,7 @@ impl Default for CosmicCompConfig {
appearance_settings: AppearanceConfig::default(),
cursor_hide_timeout: None,
activation_policy: ActivationPolicy::default(),
workspace_assignments: Vec::new(),
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions resources/i18n/en/cosmic_comp.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ window-menu-move = Move
window-menu-resize = Resize
window-menu-move-prev-workspace = Move to previous workspace
window-menu-move-next-workspace = Move to next workspace
window-menu-pin-to-workspace = Always open on this workspace
window-menu-pinned-to-workspace = Always opens on workspace {$number}
window-menu-stack = Create window stack
window-menu-unstack-all = Unstack windows
window-menu-unstack = Unstack window
Expand Down
8 changes: 8 additions & 0 deletions src/config/key_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ pub enum PrivateAction {
shortcuts::action::ResizeEdge,
shortcuts::State,
),
/// Opens the focused window's context menu at its top-left corner.
/// Hardcoded to Super+M rather than routed through the configurable
/// shortcuts system, so it works independent of cosmic-settings-config:
/// unlike a mouse right-click on the (server-side-decoration-only)
/// title bar, this reaches windows with client-side decorations too
/// (the majority of modern GTK4/Qt6/Electron apps), which otherwise have
/// no way to open this menu at all.
OpenWindowMenu,
}

/// Convert `cosmic_settings_config::shortcuts::State` to `smithay::backend::input::KeyState`.
Expand Down
14 changes: 12 additions & 2 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ mod types;
use cosmic::config::CosmicTk;
pub use cosmic_comp_config::EdidProduct;
use cosmic_comp_config::{
ActivationPolicy, AppearanceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior, XkbConfig,
XwaylandDescaling, XwaylandEavesdropping, ZoomConfig,
ActivationPolicy, AppearanceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior,
WorkspaceAssignment, XkbConfig, XwaylandDescaling, XwaylandEavesdropping, ZoomConfig,
input::{DeviceState as InputDeviceState, InputConfig, TouchpadOverride},
output::comp::{
OutputConfig, OutputInfo, OutputState, OutputsConfig, TransformDef, load_outputs,
Expand Down Expand Up @@ -855,6 +855,16 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
get_config::<WorkspaceConfig>(&config, "workspaces");
state.common.update_config();
}
"workspace_assignments" => {
let assignments =
get_config::<Vec<WorkspaceAssignment>>(&config, "workspace_assignments");
state.common.config.cosmic_conf.workspace_assignments = assignments.clone();
state
.common
.shell
.write()
.update_workspace_assignments(assignments);
}
"autotile" => {
let new = get_config::<bool>(&config, "autotile");
if new != state.common.config.cosmic_conf.autotile {
Expand Down
33 changes: 32 additions & 1 deletion src/input/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use cosmic_settings_config::shortcuts;
use cosmic_settings_config::shortcuts::action::{Direction, FocusDirection};
use smithay::{
input::{Seat, pointer::MotionEvent},
utils::{Point, Serial},
utils::{Point, SERIAL_COUNTER, Serial},
wayland::seat::WaylandFocus,
};
#[cfg(not(feature = "debug"))]
use tracing::info;
Expand Down Expand Up @@ -105,6 +106,36 @@ impl State {
.finish_resize(direction, edge.into());
}
}

Action::Private(PrivateAction::OpenWindowMenu) => {
let Some(surface) = seat
.get_keyboard()
.and_then(|kb| kb.current_focus())
.and_then(|focus| focus.wl_surface().map(|s| s.into_owned()))
else {
return;
};

let menu_serial = SERIAL_COUNTER.next_serial();
let res = self.common.shell.read().menu_request(
false,
&surface,
seat,
menu_serial,
// No click position to anchor to (keyboard-triggered); open
// at the window's top-left corner. menu_request derives the
// window's own geometry from the surface internally.
Point::from((0, 0)),
false,
&self.common.config,
&self.common.event_loop_handle,
);
if let Some((grab, focus)) = res {
seat.get_pointer()
.unwrap()
.set_grab(self, grab, menu_serial, focus);
}
}
}
}

Expand Down
29 changes: 29 additions & 0 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1846,6 +1846,35 @@ impl State {
)));
}

// Open the focused window's context menu (Alt+Space, the traditional
// "window system menu" shortcut). Hardcoded rather than going through
// the configurable shortcuts system - see the PrivateAction::OpenWindowMenu
// doc comment for why. NOTE: Super+M is already bound to Maximize by
// default (see /usr/share/cosmic/com.system76.CosmicSettings.Shortcuts/v1/defaults) -
// do not reuse it here, it would silently shadow that binding.
if !shortcuts_inhibited
&& event.state() == KeyState::Pressed
&& modifiers.alt
&& !modifiers.logo
&& !modifiers.ctrl
&& !modifiers.shift
&& key_matches(Keysym::space)
{
seat.supressed_keys().add(&handle, None);
return FilterResult::Intercept(Some((
Action::Private(PrivateAction::OpenWindowMenu),
shortcuts::Binding {
modifiers: shortcuts::Modifiers {
alt: true,
..Default::default()
},
keycode: None,
key: Some(Keysym::space),
description: None,
},
)));
}

if let Some(mut a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor() {
if event.state() == KeyState::Released {
let removed =
Expand Down
69 changes: 69 additions & 0 deletions src/shell/grabs/menu/default.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use cosmic_comp_config::WorkspaceAssignment;
use cosmic_config::ConfigSet;
use cosmic_settings_config::shortcuts::Action;
use smithay::{
input::pointer::MotionEvent, reexports::wayland_server::protocol::wl_surface::WlSurface,
Expand All @@ -16,6 +18,7 @@ use crate::{
utils::{prelude::SeatExt, screenshot::screenshot_window},
wayland::protocols::workspace::WorkspaceHandle,
};
use tracing::error;

use super::{Item, ResizeEdge};

Expand Down Expand Up @@ -246,6 +249,7 @@ pub fn window_items(
let screenshot_clone = window.clone();
let stack_clone = window.clone();
let sticky_clone = window.clone();
let pin_clone = window.clone();
let close_clone = window.clone();

vec![
Expand Down Expand Up @@ -545,6 +549,71 @@ pub fn window_items(
})
.toggled(is_sticky),
),
Some({
let app_id = window.active_window().app_id();
let pinned_workspace = config
.cosmic_conf
.workspace_assignments
.iter()
.find(|rule| rule.app_id == app_id)
.map(|rule| rule.workspace);
let label = match pinned_workspace {
Some(number) => {
fl!("window-menu-pinned-to-workspace", number = (number as usize))
}
None => fl!("window-menu-pin-to-workspace"),
};
Item::new(label, move |handle| {
let app_id = app_id.clone();
let mapped = pin_clone.clone();
let _ = handle.insert_idle(move |state| {
let mut assignments = state
.common
.config
.cosmic_conf
.workspace_assignments
.clone();
if assignments.iter().any(|rule| rule.app_id == app_id) {
assignments.retain(|rule| rule.app_id != app_id);
} else {
let shell = state.common.shell.read();
let window = mapped.active_window();
let Some(wl_surface) = window.wl_surface() else {
return;
};
let Some((workspace_handle, output)) =
shell.workspace_for_surface(&wl_surface)
else {
return;
};
let Some(idx) = shell
.workspaces
.spaces_for_output(&output)
.position(|space| space.handle == workspace_handle)
else {
return;
};
drop(shell);
assignments.push(WorkspaceAssignment {
app_id: app_id.clone(),
workspace: (idx + 1) as u32,
});
}
// Persist via cosmic-config; the compositor's own config
// watcher applies it (and other clients see it too).
if let Err(err) = state
.common
.config
.cosmic_helper
.set("workspace_assignments", &assignments)
{
error!(?err, "Failed to update workspace_assignments key");
}
});
})
.toggled(pinned_workspace.is_some())
.disabled(is_sticky)
}),
Some(Item::Separator),
if is_stacked {
Some(Item::new(fl!("window-menu-close-all"), move |_handle| {
Expand Down
32 changes: 28 additions & 4 deletions src/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::{
},
};
use cosmic_comp_config::{
AppearanceConfig, TileBehavior, ZoomConfig, ZoomMovement,
AppearanceConfig, TileBehavior, WorkspaceAssignment, ZoomConfig, ZoomMovement,
workspace::{PinnedWorkspace, WorkspaceLayout, WorkspaceMode},
};
use cosmic_config::ConfigSet;
Expand Down Expand Up @@ -296,6 +296,7 @@ pub struct Shell {
zoom_state: Option<ZoomState>,
appearance_conf: AppearanceConfig,
tiling_exceptions: TilingExceptions,
workspace_assignments: Vec<WorkspaceAssignment>,

#[cfg(feature = "debug")]
pub debug_active: bool,
Expand Down Expand Up @@ -400,7 +401,8 @@ fn create_workspace(
WorkspaceCapabilities::Activate
| WorkspaceCapabilities::SetTilingState
| WorkspaceCapabilities::Pin
| WorkspaceCapabilities::Move,
| WorkspaceCapabilities::Move
| WorkspaceCapabilities::Rename,
);
Workspace::new(
workspace_handle,
Expand Down Expand Up @@ -440,7 +442,8 @@ fn create_workspace_from_pinned(
WorkspaceCapabilities::Activate
| WorkspaceCapabilities::SetTilingState
| WorkspaceCapabilities::Pin
| WorkspaceCapabilities::Move,
| WorkspaceCapabilities::Move
| WorkspaceCapabilities::Rename,
);

if let Some(ref name) = pinned.name {
Expand Down Expand Up @@ -1713,6 +1716,7 @@ impl Shell {
appearance_conf: config.cosmic_conf.appearance_settings,
zoom_state: None,
tiling_exceptions,
workspace_assignments: config.cosmic_conf.workspace_assignments.clone(),

#[cfg(feature = "debug")]
debug_active: false,
Expand Down Expand Up @@ -2839,14 +2843,30 @@ impl Shell {
};

let pending_activation = self.pending_activations.remove(&(&window).into());
let workspace_handle = match pending_activation {
let mut workspace_handle = match pending_activation {
Some(ActivationContext::Workspace(handle)) => Some(handle),
_ => None,
};

let should_be_fullscreen = output.is_some();
let mut output = output.unwrap_or_else(|| seat.active_output());

// App->workspace assignment: if this app_id has a configured home
// workspace, send the window there. Deliberately overrides any
// activation context, so the rule holds no matter where the app was
// launched from. `workspace` in the config is 1-based.
if let Some(rule) = {
let app_id = window.app_id();
self.workspace_assignments
.iter()
.find(|rule| rule.app_id == app_id)
} {
let idx = rule.workspace.saturating_sub(1) as usize;
if let Some(ws) = self.workspaces.get(idx, &output) {
workspace_handle = Some(ws.handle);
}
}

// this is beyond stupid, just to make the borrow checker happy
let workspace = if let Some(handle) = workspace_handle.filter(|handle| {
self.workspaces
Expand Down Expand Up @@ -5017,6 +5037,10 @@ impl Shell {
self.tiling_exceptions = layout::TilingExceptions::new(exceptions);
}

pub fn update_workspace_assignments(&mut self, assignments: Vec<WorkspaceAssignment>) {
self.workspace_assignments = assignments;
}

pub fn take_presentation_feedback(
&self,
output: &Output,
Expand Down
14 changes: 14 additions & 0 deletions src/wayland/handlers/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ impl WorkspaceHandler for State {
}
}
}
Request::Rename { workspace, name } => {
let mut shell = self.common.shell.write();
if let Some(workspace_) = shell.workspaces.space_for_handle_mut(&workspace) {
workspace_.name = Some(name.clone());
let pinned = workspace_.pinned;
self.common
.workspace_state
.update()
.set_workspace_name(&workspace, name);
if pinned {
shell.workspaces.persist(&self.common.config);
}
}
}
Request::MoveBefore {
workspace,
other_workspace,
Expand Down