Prune unused empty workspaces while preserving visible outputs

This commit is contained in:
Samuel Onoja committed 2026-09-27 12:16:50 +01:00
1 parent 0295b5be3e
commit f87039562b
5 files changed
+148 -2

No files matched your search

+52
View File
@@ -110,6 +110,37 @@ impl OutputWorkspaceMap {
self.assignments.get(&workspace).copied()
}
pub fn forget_workspace(&mut self, workspace: WorkspaceId) -> bool {
if self
.outputs
.values()
.any(|output| output.active == workspace)
{
return false;
}
if let Some(owner) = self.assignments.remove(&workspace)
&& let Some(output) = self.outputs.get_mut(&owner)
{
output.workspaces.remove(&workspace);
}
self.revisions.remove(&workspace);
self.evacuations.retain(|_, record| {
record
.workspaces
.retain(|entry| entry.workspace != workspace);
if record.active == workspace {
if let Some(entry) = record.workspaces.first() {
record.active = entry.workspace;
} else {
return false;
}
}
true
});
debug_assert!(self.validate());
true
}
pub fn connected_outputs(&self) -> impl Iterator<Item = OutputId> + '_ {
self.outputs.keys().copied()
}
@@ -379,6 +410,27 @@ impl OutputWorkspaceMap {
#[cfg(test)]
mod tests {
#[test]
fn forgetting_an_inactive_workspace_prevents_hotplug_resurrection() {
let mut outputs = super::OutputWorkspaceMap::default();
let display = super::OutputId(1);
let geometry = super::OutputGeometry::new(0, 0, 1920, 1080);
let first = super::WorkspaceId(1);
let second = super::WorkspaceId(2);
outputs.connect(display, geometry, first).unwrap();
assert!(!outputs.forget_workspace(first));
outputs.switch_workspace(display, second).unwrap();
assert!(outputs.forget_workspace(first));
assert_eq!(outputs.output_for_workspace(first), None);
outputs.disconnect(display).unwrap();
outputs
.connect(display, geometry, super::WorkspaceId(3))
.unwrap();
assert_eq!(outputs.active_workspace(display), Some(second));
assert_eq!(outputs.output_for_workspace(first), None);
assert!(outputs.validate());
}
#[test]
fn changing_output_geometry_preserves_workspace_ownership_and_focus() {
let mut outputs = super::OutputWorkspaceMap::default();
+78
View File
@@ -523,6 +523,38 @@ impl WorkspaceSet {
self.workspaces.values()
}
pub fn prune_empty(&mut self, protected: &HashSet<WorkspaceId>) -> Vec<WorkspaceId> {
let mut empty = self
.workspaces
.values()
.filter(|workspace| {
workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty()
})
.map(|workspace| workspace.id)
.collect::<Vec<_>>();
empty.sort_by_key(|id| id.0);
let visible_empty = empty
.iter()
.any(|id| *id == self.active || protected.contains(id));
let spare = if visible_empty {
None
} else {
empty.first().copied()
};
let mut removed = Vec::new();
for id in empty {
if id == self.active || protected.contains(&id) || Some(id) == spare {
continue;
}
if let Some(workspace) = self.workspaces.remove(&id) {
self.names.remove(&workspace.name);
removed.push(id);
}
}
debug_assert!(self.validate().is_ok());
removed
}
pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> {
self.window_workspaces.get(&window).copied()
}
@@ -1068,6 +1100,52 @@ fn finite_or_zero(value: f64) -> f64 {
#[cfg(test)]
mod tests {
#[test]
fn pruning_keeps_windows_and_one_empty_workspace() {
let mut set = super::WorkspaceSet::default();
let first = set.active_id();
set.insert_window(super::WindowId(1), super::Axis::Horizontal, 0.5)
.unwrap();
let floating = set.ensure_numeric(2).unwrap();
set.insert_floating_window(
super::WindowId(2),
floating,
super::Rect::new(0., 0., 100., 100.),
false,
)
.unwrap();
let spare = set.ensure_numeric(3).unwrap();
let excess = set.ensure_numeric(4).unwrap();
assert_eq!(
set.prune_empty(&std::collections::HashSet::new()),
vec![excess]
);
assert!(set.workspace(first).is_some());
assert!(set.workspace(floating).is_some());
assert!(set.workspace(spare).is_some());
set.activate(spare).unwrap();
set.remove_window(super::WindowId(1)).unwrap();
assert_eq!(
set.prune_empty(&std::collections::HashSet::new()),
vec![first]
);
assert!(set.validate().is_ok());
assert_ne!(set.ensure_numeric(1).unwrap(), first);
}
#[test]
fn pruning_never_removes_another_displays_active_workspace() {
let mut set = super::WorkspaceSet::default();
let first = set.active_id();
let second = set.ensure_numeric(2).unwrap();
let third = set.ensure_numeric(3).unwrap();
assert_eq!(
set.prune_empty(&std::collections::HashSet::from([first, second])),
vec![third]
);
assert!(set.workspace(second).is_some());
}
#[test]
fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() {
let mut workspaces = WorkspaceSet::default();