Skip to content

Commit 4726071

Browse files
committed
config: workspace empty behavior
1 parent 57715b6 commit 4726071

43 files changed

Lines changed: 2201 additions & 79 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

book/src/control-center.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ LIBEI_SOCKET
6464
Workspace Display Order
6565
: Dropdown to select how workspaces are ordered in the bar
6666

67+
Workspace Empty Behavior
68+
: Dropdown to select what happens to empty workspaces when they are left or become inactive
69+
6770
Log Level
6871
: Dropdown to change the active log level at runtime (shown when the logger is available)
6972

book/src/workspaces.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,51 @@ workspace-display-order = "sorted"
153153

154154
You can also change this at runtime in the control center.
155155

156+
## Empty Workspace Behavior
157+
158+
Jay creates workspaces on demand. When a workspace becomes empty, Jay can
159+
optionally hide or destroy it automatically so your workspace list does not
160+
accumulate unused entries.
161+
162+
Configure this with the `workspace-empty-behavior` top-level setting (or at
163+
runtime in the control center, in the Compositor pane):
164+
165+
```toml
166+
workspace-empty-behavior = "hide-on-leave"
167+
```
168+
169+
> [!NOTE]
170+
> This behavior is evaluated per output.
171+
>
172+
> - "leave" means the workspace stops being the active workspace on its output
173+
> because you showed another workspace on that same output.
174+
> - "inactive" means the workspace is currently not the active workspace on its
175+
> output.
176+
177+
Supported values:
178+
179+
`preserve`
180+
: Never destroy or hide empty workspaces automatically.
181+
182+
`destroy-on-leave`
183+
: Destroy an empty workspace when you leave it (default).
184+
185+
`hide-on-leave`
186+
: Hide an empty workspace when you leave it.
187+
188+
`destroy`
189+
: Destroy an empty workspace whenever it is empty and inactive.
190+
191+
`hide`
192+
: Hide an empty workspace whenever it is empty and inactive.
193+
194+
> [!TIP]
195+
> Hidden workspaces are omitted from Jay's built-in workspace lists and the bar.
196+
> Some external workspace tools may still show them as hidden. You can restore
197+
> them by showing the workspace by name (for example via the `show-workspace`
198+
> action). When restoring a hidden workspace, Jay prefers the output it was last
199+
> shown on if that output is still connected.
200+
156201
## Hot-Plug and Hot-Unplug
157202

158203
Jay handles monitor connections gracefully:

jay-config/src/_private/client.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use {
3737
ContentType, MatchedWindow, TileState, Window, WindowCriterion, WindowMatcher,
3838
WindowType,
3939
},
40-
workspace::WorkspaceDisplayOrder,
40+
workspace::{WorkspaceDisplayOrder, WorkspaceEmptyBehavior},
4141
xwayland::XScalingMode,
4242
},
4343
bincode::Options,
@@ -1100,6 +1100,10 @@ impl ConfigClient {
11001100
self.send(&ClientMessage::SetWorkspaceDisplayOrder { order });
11011101
}
11021102

1103+
pub fn set_workspace_empty_behavior(&self, behavior: WorkspaceEmptyBehavior) {
1104+
self.send(&ClientMessage::SetWorkspaceEmptyBehavior { behavior });
1105+
}
1106+
11031107
pub fn seat_create_mark(&self, seat: Seat, kc: Option<u32>) {
11041108
self.send(&ClientMessage::SeatCreateMark { seat, kc });
11051109
}

jay-config/src/_private/ipc.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use {
1919
Transform, VrrMode, connector_type::ConnectorType,
2020
},
2121
window::{ContentType, TileState, Window, WindowMatcher, WindowType},
22-
workspace::WorkspaceDisplayOrder,
22+
workspace::{WorkspaceDisplayOrder, WorkspaceEmptyBehavior},
2323
xwayland::XScalingMode,
2424
},
2525
serde::{Deserialize, Serialize},
@@ -893,6 +893,9 @@ pub enum ClientMessage<'a> {
893893
ShowWorkspace3 {
894894
v1: WorkspaceShowOpV1,
895895
},
896+
SetWorkspaceEmptyBehavior {
897+
behavior: WorkspaceEmptyBehavior,
898+
},
896899
}
897900

898901
#[derive(Serialize, Deserialize, Debug)]

jay-config/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,9 @@ impl Workspace {
176176

177177
/// Moves this workspace to another output.
178178
///
179-
/// This has no effect if the workspace is not currently being shown.
179+
/// Hidden workspaces remain hidden and are restored on the new output when shown again.
180+
///
181+
/// This has no effect if the workspace does not exist or the output is not connected.
180182
pub fn move_to_output(self, output: Connector) {
181183
get!().move_to_output(WorkspaceSource::Explicit(self), output);
182184
}

jay-config/src/workspace.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,25 @@ pub enum WorkspaceDisplayOrder {
1717
pub fn set_workspace_display_order(order: WorkspaceDisplayOrder) {
1818
get!().set_workspace_display_order(order);
1919
}
20+
21+
/// Configures what happens to empty workspaces when they are left or become inactive.
22+
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
23+
pub enum WorkspaceEmptyBehavior {
24+
/// Never destroy or hide empty workspaces automatically.
25+
Preserve,
26+
/// Destroy an empty workspace when switching away from it.
27+
DestroyOnLeave,
28+
/// Hide an empty workspace when switching away from it.
29+
HideOnLeave,
30+
/// Destroy an empty workspace whenever it is empty and inactive.
31+
Destroy,
32+
/// Hide an empty workspace whenever it is empty and inactive.
33+
Hide,
34+
}
35+
36+
/// Sets what should happen to empty workspaces.
37+
///
38+
/// The default is `WorkspaceEmptyBehavior::DestroyOnLeave`.
39+
pub fn set_workspace_empty_behavior(behavior: WorkspaceEmptyBehavior) {
40+
get!().set_workspace_empty_behavior(behavior);
41+
}

src/compositor.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ use {
6060
tracy::enable_profiler,
6161
tree::{
6262
DisplayNode, NodeIds, OutputNode, TearingMode, Transform, VrrMode,
63-
WorkspaceDisplayOrder, container_layout, container_render_positions,
64-
container_render_titles, float_layout, float_titles, output_render_data,
65-
placeholder_render_textures,
63+
WorkspaceDisplayOrder, WorkspaceEmptyBehavior, container_layout,
64+
container_render_positions, container_render_titles, float_layout, float_titles,
65+
output_render_data, placeholder_render_textures,
6666
},
6767
user_session::import_environment,
6868
utils::{
@@ -390,6 +390,7 @@ fn start_compositor2(
390390
enable_primary_selection: Cell::new(true),
391391
xdg_surface_configure_events: Default::default(),
392392
workspace_display_order: Cell::new(WorkspaceDisplayOrder::Manual),
393+
workspace_empty_behavior: Cell::new(WorkspaceEmptyBehavior::DestroyOnLeave),
393394
outputs_without_hc: Default::default(),
394395
udmabuf: Default::default(),
395396
gfx_ctx_changed: Default::default(),

src/config/handler.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ use {
7474
VrrMode as ConfigVrrMode,
7575
},
7676
window::{TileState as ConfigTileState, Window, WindowMatcher},
77-
workspace::WorkspaceDisplayOrder,
77+
workspace::{WorkspaceDisplayOrder, WorkspaceEmptyBehavior},
7878
xwayland::XScalingMode,
7979
},
8080
kbvm::Keycode,
@@ -572,6 +572,9 @@ impl ConfigProxyHandler {
572572
fn handle_get_workspaces(&self) {
573573
let mut workspaces = vec![];
574574
for ws in self.state.workspaces.lock().values() {
575+
if ws.hidden.get() {
576+
continue;
577+
}
575578
workspaces.push(self.get_workspace_by_name(&ws.name));
576579
}
577580
self.respond(Response::GetWorkspaces { workspaces });
@@ -1143,7 +1146,11 @@ impl ConfigProxyHandler {
11431146
seat,
11441147
}
11451148
};
1146-
if move_ {
1149+
let hidden = ws.hidden.get();
1150+
if move_ && hidden {
1151+
self.state.show_workspace2(seat.as_ref(), &output, &ws);
1152+
}
1153+
if move_ && !hidden {
11471154
move_ws_to_output(
11481155
&ws,
11491156
&output,
@@ -1155,9 +1162,11 @@ impl ConfigProxyHandler {
11551162
},
11561163
);
11571164
if let Some(seat) = &seat {
1158-
ws.node_do_focus(seat, crate::tree::Direction::Unspecified);
1165+
ws.clone()
1166+
.node_do_focus(seat, crate::tree::Direction::Unspecified);
11591167
}
1160-
} else {
1168+
}
1169+
if !move_ {
11611170
self.state.show_workspace2(seat.as_ref(), &output, &ws);
11621171
}
11631172
if let Some(seat) = &seat {
@@ -1173,6 +1182,14 @@ impl ConfigProxyHandler {
11731182
Some(ws) => ws,
11741183
_ => seat.get_fallback_output().create_workspace(name.deref()),
11751184
};
1185+
if workspace.hidden.get()
1186+
&& self
1187+
.state
1188+
.restore_hidden_workspace(&workspace, Some(seat.get_fallback_output()), Some(&seat))
1189+
.is_none()
1190+
{
1191+
return Ok(());
1192+
}
11761193
seat.set_workspace(&workspace);
11771194
Ok(())
11781195
}
@@ -1187,6 +1204,14 @@ impl ConfigProxyHandler {
11871204
_ => return Ok(()),
11881205
},
11891206
};
1207+
if workspace.hidden.get()
1208+
&& self
1209+
.state
1210+
.restore_hidden_workspace(&workspace, window.node_output(), None)
1211+
.is_none()
1212+
{
1213+
return Ok(());
1214+
}
11901215
toplevel_set_workspace(&self.state, window, &workspace);
11911216
Ok(())
11921217
}
@@ -1559,6 +1584,10 @@ impl ConfigProxyHandler {
15591584
self.state.set_workspace_display_order(order.into());
15601585
}
15611586

1587+
fn handle_set_workspace_empty_behavior(&self, behavior: WorkspaceEmptyBehavior) {
1588+
self.state.set_workspace_empty_behavior(behavior.into());
1589+
}
1590+
15621591
fn handle_get_seat_float_pinned(&self, seat: Seat) -> Result<(), CphError> {
15631592
let seat = self.get_seat(seat)?;
15641593
self.respond(Response::GetFloatPinned {
@@ -1750,6 +1779,7 @@ impl ConfigProxyHandler {
17501779
fn handle_get_workspace_connector(&self, workspace: Workspace) -> Result<(), CphError> {
17511780
let connector = self
17521781
.get_existing_workspace(workspace)?
1782+
.filter(|ws| !ws.hidden.get())
17531783
.map(|ws| ws.output.get())
17541784
.filter(|o| !o.is_dummy)
17551785
.map(|o| Connector(o.global.connector.id.raw() as _))
@@ -3574,6 +3604,9 @@ impl ConfigProxyHandler {
35743604
ClientMessage::ShowWorkspace3 { v1 } => {
35753605
self.handle_show_workspace_3(v1).wrn("show_workspace_3")?
35763606
}
3607+
ClientMessage::SetWorkspaceEmptyBehavior { behavior } => {
3608+
self.handle_set_workspace_empty_behavior(behavior)
3609+
}
35773610
}
35783611
Ok(())
35793612
}

src/control_center/cc_compositor.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ impl CompositorPane {
5757
s.workspace_display_order.get(),
5858
|o| s.set_workspace_display_order(o),
5959
);
60+
combo_box(
61+
ui,
62+
"Workspace Empty Behavior",
63+
s.workspace_empty_behavior.get(),
64+
|b| s.set_workspace_empty_behavior(b),
65+
);
6066
if let Some(logger) = &s.logger {
6167
combo_box(ui, "Log Level", logger.level(), |l| s.set_log_level(l));
6268
row(ui, "Log File", |ui| {

src/ifs/jay_compositor.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,9 @@ impl JayCompositorRequestHandler for JayCompositor {
332332
.workspace_watchers
333333
.set((self.client.id, req.id), watcher.clone());
334334
for ws in self.client.state.workspaces.lock().values() {
335+
if ws.hidden.get() {
336+
continue;
337+
}
335338
watcher.send_workspace(ws)?;
336339
}
337340
Ok(())

0 commit comments

Comments
 (0)