Add per-monitor workspace back-and-forth

This commit is contained in:
Samuel Onoja committed 2026-10-01 10:14:35 +01:00
1 parent cb6fe73e44
commit 17b8482433
12 files changed
+405 -20

No files matched your search

+47 -3
View File
@@ -27,6 +27,8 @@ pub struct Config {
#[serde(default)]
layout: LayoutConfig,
#[serde(default)]
workspaces: WorkspacesConfig,
#[serde(default)]
input: InputConfig,
#[serde(default)]
theme: ThemeConfig,
@@ -315,6 +317,7 @@ impl Config {
lock_idle: self.lock_screen.validate()?,
autostart: self.autostart.clone(),
layout_mode: self.layout_mode(),
workspace_auto_back_and_forth: self.workspaces.auto_back_and_forth,
gap_config: self.gap_config()?,
input_settings,
bindings,
@@ -500,6 +503,7 @@ pub enum BindingAction {
Resize(ferese_layout::Direction),
SwitchWorkspace(u8),
SwitchRelativeWorkspace(bool),
WorkspaceBackAndForth,
MoveToWorkspace(u8),
ToggleFullscreen,
ToggleMaximized,
@@ -598,6 +602,7 @@ impl Binding {
BindingAction::Move(direction) => format!("Move window {direction:?}"),
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
BindingAction::WorkspaceBackAndForth => "Return to last workspace on this monitor".into(),
BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"),
BindingAction::SwitchRelativeWorkspace(next) => {
if *next { "Next workspace" } else { "Previous workspace" }.into()
@@ -818,6 +823,12 @@ impl Default for SpringSettings {
}
}
#[derive(Debug, Default, Deserialize)]
struct WorkspacesConfig {
#[serde(default)]
auto_back_and_forth: bool,
}
#[derive(Debug, Deserialize)]
struct LayoutConfig {
mode: Option<LayoutModeValue>,
@@ -1495,6 +1506,10 @@ fn parse_action(
no_argument()?;
Ok(BindingAction::SwitchRelativeWorkspace(action == "workspace-next"))
}
"workspace-back-and-forth" => {
no_argument()?;
Ok(BindingAction::WorkspaceBackAndForth)
}
"spawn" => {
let command = required_argument()?;
let argv = commands
@@ -1591,6 +1606,7 @@ fn default_bindings() -> Vec<BindingConfig> {
binding("Super+]", "expel", None),
binding("Super+Shift+Space", "toggle-floating", None),
binding("Super+Tab", "toggle-overview", None),
binding("Super+Escape", "workspace-back-and-forth", None),
binding("Super+Shift+E", "exit", None),
];
@@ -1872,6 +1888,34 @@ pub(crate) fn config_path() -> Option<PathBuf> {
mod tests {
use super::*;
#[test]
fn workspace_back_and_forth_config_is_optional_and_bindable() {
let defaults = Config::default().runtime_config().unwrap();
assert!(!defaults.workspace_auto_back_and_forth);
assert!(
defaults
.bindings
.iter()
.any(|binding| binding.action == BindingAction::WorkspaceBackAndForth)
);
let configured = parse(
"workspaces {\n auto-back-and-forth #true\n}\nbinding \"Super+BackSpace\" \"workspace-back-and-forth\"\n",
)
.runtime_config()
.unwrap();
assert!(configured.workspace_auto_back_and_forth);
assert_eq!(
configured
.bindings
.iter()
.filter(|binding| binding.action == BindingAction::WorkspaceBackAndForth)
.count(),
2
);
assert!(parse_action("workspace-back-and-forth", Some("2"), &HashMap::new()).is_err());
assert!(Config::parse_source("workspaces {\n auto-back-and-forth \"yes\"\n}\n").is_err());
}
#[test]
fn packaged_and_custom_kdl_pass_runtime_validation() {
Config::parse_source(include_str!("../../packaging/config.kdl"))
@@ -2328,7 +2372,7 @@ mod tests {
let input = config.input_settings().unwrap();
let bindings = config.bindings(&input).unwrap();
assert_eq!(bindings.len(), 48);
assert_eq!(bindings.len(), 49);
for (shift, action) in [
(false, BindingAction::ToggleMaximized),
(true, BindingAction::ToggleFullscreen),
@@ -2371,14 +2415,14 @@ mod tests {
let input = replaced.input_settings().unwrap();
let bindings = replaced.bindings(&input).unwrap();
assert_eq!(bindings.len(), 48);
assert_eq!(bindings.len(), 49);
assert!(bindings.iter().any(|binding| {
binding.action == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()])
}));
let input = unbound.input_settings().unwrap();
let bindings = unbound.bindings(&input).unwrap();
assert_eq!(bindings.len(), 47);
assert_eq!(bindings.len(), 48);
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
}
+2 -1
View File
@@ -987,8 +987,9 @@ impl Ferese {
BindingAction::Move(direction) => self.move_direction(direction),
BindingAction::Resize(direction) => self.resize_direction(direction),
BindingAction::SwitchRelativeWorkspace(next) => self.switch_relative_workspace(next, None),
BindingAction::WorkspaceBackAndForth => self.workspace_back_and_forth(),
BindingAction::SwitchWorkspace(workspace) => {
self.switch_workspace(u32::from(workspace));
self.switch_workspace_from_binding(u32::from(workspace));
}
BindingAction::MoveToWorkspace(workspace) => {
self.move_focused_to_workspace(u32::from(workspace));
+1
View File
@@ -650,6 +650,7 @@ impl Ferese {
"move" => self.move_direction(direction_arg(args)?),
"resize" => self.resize_direction(direction_arg(args)?),
"workspace" => self.switch_workspace(workspace_arg(args)?),
"workspace-back-and-forth" => self.workspace_back_and_forth(),
"move-to-workspace" => self.move_focused_to_workspace(workspace_arg(args)?),
"toggle-floating" => self.toggle_focused_floating(),
"toggle-fullscreen" => self.toggle_focused_fullscreen(),
+36 -5
View File
@@ -276,6 +276,7 @@ pub struct Ferese {
gap_config: GapConfig,
pub(crate) input_settings: InputSettings,
pub(crate) bindings: Vec<Binding>,
workspace_auto_back_and_forth: bool,
window_rules: Vec<WindowRule>,
window_rules_applied: HashSet<WindowId>,
pub(crate) theme_settings: ThemeSettings,
@@ -354,6 +355,7 @@ pub struct RuntimeConfig {
pub gap_config: GapConfig,
pub input_settings: InputSettings,
pub bindings: Vec<Binding>,
pub workspace_auto_back_and_forth: bool,
pub window_rules: Vec<WindowRule>,
pub theme_settings: ThemeSettings,
pub default_column_width: ColumnWidth,
@@ -495,6 +497,7 @@ impl Ferese {
gap_config: config.gap_config,
input_settings: config.input_settings,
bindings: config.bindings,
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
window_rules: config.window_rules,
window_rules_applied: HashSet::new(),
theme_settings: config.theme_settings,
@@ -631,6 +634,7 @@ impl Ferese {
self.gap_config = config.gap_config;
self.input_settings = config.input_settings;
self.bindings = config.bindings;
self.workspace_auto_back_and_forth = config.workspace_auto_back_and_forth;
self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings);
let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules);
self.theme_settings = config.theme_settings;
@@ -1505,7 +1509,9 @@ impl Ferese {
}
fn prune_empty_workspaces(&mut self, visible: &HashSet<WorkspaceId>) {
for workspace in self.workspaces.prune_empty(visible) {
let mut protected = visible.clone();
protected.extend(self.output_workspaces.history_workspaces());
for workspace in self.workspaces.prune_empty(&protected) {
self.output_workspaces.forget_workspace(workspace);
self.viewport_animations.remove(&workspace);
}
@@ -2768,14 +2774,32 @@ impl Ferese {
}
pub fn switch_workspace(&mut self, index: u32) {
self.switch_workspace_internal(index, None);
self.switch_workspace_internal(index, None, false);
}
pub(crate) fn switch_workspace_from_binding(&mut self, index: u32) {
self.switch_workspace_internal(index, None, self.workspace_auto_back_and_forth);
}
pub(crate) fn workspace_back_and_forth(&mut self) {
let Some(output) = self.output_workspaces.focused_output() else {
return;
};
if let Some(workspace) = self.output_workspaces.previous_workspace(output) {
self.activate_managed_workspace(workspace);
}
}
pub(crate) fn switch_workspace_from_swipe(&mut self, index: u32, direction: SwipeDirection) {
self.switch_workspace_internal(index, Some(direction));
self.switch_workspace_internal(index, Some(direction), false);
}
fn switch_workspace_internal(&mut self, index: u32, slide_direction: Option<SwipeDirection>) {
fn switch_workspace_internal(
&mut self,
index: u32,
slide_direction: Option<SwipeDirection>,
auto_back_and_forth: bool,
) {
let workspace = match self.workspaces.ensure_numeric(index) {
Ok(workspace) => workspace,
Err(error) => {
@@ -2787,7 +2811,10 @@ impl Ferese {
return;
};
let previous = self.output_workspaces.active_workspace(output);
let owner = match self.output_workspaces.switch_workspace(output, workspace) {
let owner = match self
.output_workspaces
.select_workspace(output, workspace, auto_back_and_forth)
{
Ok(ferese_core::WorkspaceSwitch::Activated(output))
| Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output,
Err(error) => {
@@ -2795,6 +2822,10 @@ impl Ferese {
return;
}
};
let workspace = self
.output_workspaces
.active_workspace(owner)
.expect("workspace owner is connected");
self.update_workspace_slide(output, owner, previous, workspace, slide_direction);
self.activate_output_workspace(owner, workspace);