Skip to content

Commit 35a7d00

Browse files
committed
config: workspace empty behavior
1 parent 7d65326 commit 35a7d00

43 files changed

Lines changed: 2221 additions & 82 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: 35 additions & 3 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,
@@ -585,6 +585,9 @@ impl ConfigProxyHandler {
585585
fn handle_get_workspaces(&self) {
586586
let mut workspaces = vec![];
587587
for ws in self.state.workspaces.lock().values() {
588+
if ws.hidden.get() {
589+
continue;
590+
}
588591
workspaces.push(self.get_workspace_by_name(&ws.name, ws.ty));
589592
}
590593
self.respond(Response::GetWorkspaces { workspaces });
@@ -1159,7 +1162,11 @@ impl ConfigProxyHandler {
11591162
seat,
11601163
}
11611164
};
1162-
if move_ {
1165+
let hidden = ws.hidden.get();
1166+
if move_ && hidden {
1167+
self.state.show_workspace2(seat.as_ref(), &output, &ws);
1168+
}
1169+
if move_ && !hidden {
11631170
move_ws_to_output(
11641171
&ws,
11651172
&output,
@@ -1173,7 +1180,8 @@ impl ConfigProxyHandler {
11731180
if let Some(seat) = &seat {
11741181
ws.do_focus(seat, crate::tree::Direction::Unspecified);
11751182
}
1176-
} else {
1183+
}
1184+
if !move_ {
11771185
self.state.show_workspace2(seat.as_ref(), &output, &ws);
11781186
}
11791187
if let Some(seat) = &seat {
@@ -1193,6 +1201,14 @@ impl ConfigProxyHandler {
11931201
}
11941202
},
11951203
};
1204+
if workspace.hidden.get()
1205+
&& self
1206+
.state
1207+
.restore_hidden_workspace(&workspace, Some(seat.get_fallback_output()), Some(&seat))
1208+
.is_none()
1209+
{
1210+
return Ok(());
1211+
}
11961212
seat.set_workspace(&workspace);
11971213
Ok(())
11981214
}
@@ -1209,6 +1225,14 @@ impl ConfigProxyHandler {
12091225
},
12101226
},
12111227
};
1228+
if workspace.hidden.get()
1229+
&& self
1230+
.state
1231+
.restore_hidden_workspace(&workspace, window.node_output(), None)
1232+
.is_none()
1233+
{
1234+
return Ok(());
1235+
}
12121236
toplevel_set_workspace(&self.state, window, &workspace);
12131237
Ok(())
12141238
}
@@ -1581,6 +1605,10 @@ impl ConfigProxyHandler {
15811605
self.state.set_workspace_display_order(order.into());
15821606
}
15831607

1608+
fn handle_set_workspace_empty_behavior(&self, behavior: WorkspaceEmptyBehavior) {
1609+
self.state.set_workspace_empty_behavior(behavior.into());
1610+
}
1611+
15841612
fn handle_get_seat_float_pinned(&self, seat: Seat) -> Result<(), CphError> {
15851613
let seat = self.get_seat(seat)?;
15861614
self.respond(Response::GetFloatPinned {
@@ -1773,6 +1801,7 @@ impl ConfigProxyHandler {
17731801
fn handle_get_workspace_connector(&self, workspace: Workspace) -> Result<(), CphError> {
17741802
let connector = self
17751803
.get_existing_workspace(workspace)?
1804+
.filter(|ws| !ws.hidden.get())
17761805
.map(|ws| ws.output.get())
17771806
.filter(|o| !o.is_dummy)
17781807
.map(|o| Connector(o.global.connector.id.raw() as _))
@@ -3597,6 +3626,9 @@ impl ConfigProxyHandler {
35973626
ClientMessage::ShowWorkspace3 { v1 } => {
35983627
self.handle_show_workspace_3(v1).wrn("show_workspace_3")?
35993628
}
3629+
ClientMessage::SetWorkspaceEmptyBehavior { behavior } => {
3630+
self.handle_set_workspace_empty_behavior(behavior)
3631+
}
36003632
}
36013633
Ok(())
36023634
}

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)