Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0c9832d
feat: add minimization feature and raw protobuf API
sapient-cogbag Jan 29, 2026
65062c4
fix: make activation unminimize windows ^.^
sapient-cogbag Jan 29, 2026
3d7bad0
chore: clean up commented out junk/experimental code
sapient-cogbag Jan 29, 2026
616767d
Merge branch 'main' into feat-window-minimisation
sapient-cogbag Jan 29, 2026
5786a8a
feat: warn when window missing output on set_minimize
sapient-cogbag Jan 29, 2026
641ef7a
fix: only unset focus if the to-be-minimized window is actually focused
sapient-cogbag Jan 29, 2026
4d79958
chore: cargo fmt
sapient-cogbag Jan 29, 2026
c2fdf27
fix: make config functionality clearer
sapient-cogbag Jan 30, 2026
e768db2
fix: unminimize windows if being focused
sapient-cogbag Jan 31, 2026
2a7d4cf
feat: add example of unminimisation in config
sapient-cogbag Jan 31, 2026
e0b1e9a
Merge branch 'main' into feat-window-minimisation
sapient-cogbag Jan 31, 2026
a842d94
Merge branch 'main' into feat-window-minimisation
sapient-cogbag Feb 4, 2026
749d1c7
Merge branch 'main' into feat-window-minimisation
sapient-cogbag Feb 8, 2026
d863624
fix: hide minimized x11 surfaces ^.^ nya
sapient-cogbag Feb 9, 2026
de4b69b
compat: add specific RPC return message for SetMinimized for future c…
sapient-cogbag Feb 9, 2026
c19693e
feat: initial impl of WindowRules that set minimization state
sapient-cogbag Feb 9, 2026
0dd548a
fix: correctly set x11 hidden property on surfaces ^.^
sapient-cogbag Feb 9, 2026
6b2e760
feat: replace the focusing api into a version that is fallible and ca…
sapient-cogbag Feb 10, 2026
7fc65dc
chore: update focus tests
sapient-cogbag Feb 10, 2026
de32422
feat: implement lua api for the try_focused system
sapient-cogbag Feb 11, 2026
33acbf1
Merge branch 'main' into feat-window-minimisation
sapient-cogbag Feb 11, 2026
1b89758
feat: implement lua side of the client API for minimization
sapient-cogbag Feb 11, 2026
7a6cc1d
doc: add comment explaining the need for the weird construction. We m…
sapient-cogbag Feb 11, 2026
8fc71b1
style: replace the janky Result-Ok-Err swapping mechanism with some p…
sapient-cogbag Feb 15, 2026
5a67b22
Merge branch 'main' into feat-window-minimisation
sapient-cogbag Feb 21, 2026
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
14 changes: 14 additions & 0 deletions api/protobuf/pinnacle/window/v1/window.proto
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ message GetLayoutModeResponse {
LayoutMode layout_mode = 1;
}

message GetMinimizedRequest {
uint32 window_id = 1;
}
message GetMinimizedResponse {
bool minimized = 1;
}

message GetTagIdsRequest {
uint32 window_id = 1;
}
Expand Down Expand Up @@ -116,6 +123,11 @@ message SetMaximizedRequest {
pinnacle.util.v1.SetOrToggle set_or_toggle = 2;
}

message SetMinimizedRequest {
uint32 window_id = 1;
pinnacle.util.v1.SetOrToggle set_or_toggle = 2;
}

message SetFloatingRequest {
uint32 window_id = 1;
pinnacle.util.v1.SetOrToggle set_or_toggle = 2;
Expand Down Expand Up @@ -222,6 +234,7 @@ service WindowService {
rpc GetSize(GetSizeRequest) returns (GetSizeResponse);
rpc GetFocused(GetFocusedRequest) returns (GetFocusedResponse);
rpc GetLayoutMode(GetLayoutModeRequest) returns (GetLayoutModeResponse);
rpc GetMinimized(GetMinimizedRequest) returns (GetMinimizedResponse);
rpc GetTagIds(GetTagIdsRequest) returns (GetTagIdsResponse);
rpc GetWindowsInDir(GetWindowsInDirRequest) returns (GetWindowsInDirResponse);
rpc GetForeignToplevelListIdentifier(GetForeignToplevelListIdentifierRequest) returns (GetForeignToplevelListIdentifierResponse);
Expand All @@ -231,6 +244,7 @@ service WindowService {
rpc ResizeTile(ResizeTileRequest) returns (google.protobuf.Empty);
rpc SetFullscreen(SetFullscreenRequest) returns (google.protobuf.Empty);
rpc SetMaximized(SetMaximizedRequest) returns (google.protobuf.Empty);
rpc SetMinimized(SetMinimizedRequest) returns (google.protobuf.Empty);
Comment thread
sapient-cogbag marked this conversation as resolved.
Outdated
rpc SetFloating(SetFloatingRequest) returns (google.protobuf.Empty);
rpc SetFocused(SetFocusedRequest) returns (google.protobuf.Empty);
rpc SetDecorationMode(SetDecorationModeRequest) returns (google.protobuf.Empty);
Expand Down
10 changes: 10 additions & 0 deletions api/rust/examples/default_config/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ async fn config() {
.group("Window")
.description("Toggle maximized on the focused window");

// `mod_key + n` toggles minimized
input::keybind(mod_key, 'n')
.on_press(|| {
if let Some(window) = window::get_focused() {
window.toggle_minimized();
}
Comment thread
sapient-cogbag marked this conversation as resolved.
})
.group("Window")
.description("Toggle minimized on the focused window");

// Media keybinds ------------------------------------------------------

input::keybind(Mod::empty(), Keysym::XF86_AudioRaiseVolume)
Expand Down
56 changes: 50 additions & 6 deletions api/rust/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ use pinnacle_api_defs::pinnacle::{
self,
v1::{
GetAppIdRequest, GetFocusedRequest, GetForeignToplevelListIdentifierRequest,
GetLayoutModeRequest, GetLocRequest, GetSizeRequest, GetTagIdsRequest, GetTitleRequest,
GetWindowsInDirRequest, LowerRequest, MoveGrabRequest, MoveToOutputRequest,
MoveToTagRequest, RaiseRequest, ResizeGrabRequest, ResizeTileRequest,
SetDecorationModeRequest, SetFloatingRequest, SetFocusedRequest, SetFullscreenRequest,
SetGeometryRequest, SetMaximizedRequest, SetTagRequest, SetTagsRequest,
SetVrrDemandRequest, SwapRequest,
GetLayoutModeRequest, GetLocRequest, GetMinimizedRequest, GetSizeRequest,
GetTagIdsRequest, GetTitleRequest, GetWindowsInDirRequest, LowerRequest,
MoveGrabRequest, MoveToOutputRequest, MoveToTagRequest, RaiseRequest,
ResizeGrabRequest, ResizeTileRequest, SetDecorationModeRequest, SetFloatingRequest,
SetFocusedRequest, SetFullscreenRequest, SetGeometryRequest, SetMaximizedRequest,
SetMinimizedRequest, SetTagRequest, SetTagsRequest, SetVrrDemandRequest, SwapRequest,
},
},
};
Expand Down Expand Up @@ -365,6 +365,34 @@ impl WindowHandle {
.unwrap();
}

/// Sets this window to minimized or not.
pub fn set_minimized(&self, set: bool) {
let window_id = self.id;
Client::window()
.set_minimized(SetMinimizedRequest {
window_id,
set_or_toggle: match set {
true => SetOrToggle::Set,
false => SetOrToggle::Unset,
}
.into(),
})
.block_on_tokio()
.unwrap();
}

/// Toggles this window between minimized and not.
pub fn toggle_minimized(&self) {
let window_id = self.id;
Client::window()
.set_minimized(SetMinimizedRequest {
window_id,
set_or_toggle: SetOrToggle::Toggle.into(),
})
.block_on_tokio()
.unwrap();
}

/// Sets this window to floating or not.
///
/// Floating windows will not be tiled and can be moved around and resized freely.
Expand Down Expand Up @@ -807,6 +835,22 @@ impl WindowHandle {
self.layout_mode_async().await == LayoutMode::Maximized
}

/// Gets whether or not this window is minimized
pub fn minimized(&self) -> bool {
self.minimized_async().block_on_tokio()
}

/// Async impl for [`Self::minimized`]
pub async fn minimized_async(&self) -> bool {
let window_id = self.id;
Client::window()
.get_minimized(GetMinimizedRequest { window_id })
.await
.unwrap()
.into_inner()
.minimized
}

/// Gets handles to all tags on this window.
pub fn tags(&self) -> impl Iterator<Item = TagHandle> + use<> {
self.tags_async().block_on_tokio()
Expand Down
38 changes: 37 additions & 1 deletion src/api/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,43 @@ pub fn set_geometry(
);
}

// TODO: minimized
/// Sets or toggles if a window is minimized.
///
/// Minimized windows are always unfocused.
pub fn set_minimized(state: &mut State, window: &WindowElement, set: impl Into<Option<bool>>) {
if window.is_x11_override_redirect() {
return;
}

let set = set.into();

let is_minimized = window.with_state(|state| state.minimized);
let set = match set {
Some(absolute_set) => absolute_set,
None => !is_minimized,
};

// Note: tag moving will automatically adjust the output on the window directly even if
// minimised, so we can rely on this.
let Some(output) = window.output(&state.pinnacle) else {
// No associated output, do nothing...
Comment thread
sapient-cogbag marked this conversation as resolved.
Outdated
return;
};

// This means we can rely on the output associated with the [`WindowElementState`] even while
// minimized, and we can use it to schedule layouts.
if set != is_minimized {
window.with_state_mut(|state| state.minimized = set);

state.pinnacle.request_layout(&output);
state.schedule_render(&output);
state.pinnacle.update_xwayland_stacking_order();

if !set {
state.pinnacle.keyboard_focus_stack.unset_focus();
}
}
}

/// Sets a window to focused or not.
///
Expand Down
64 changes: 55 additions & 9 deletions src/api/window/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ use pinnacle_api_defs::pinnacle::{
self, CloseRequest, GetAppIdRequest, GetAppIdResponse, GetFocusedRequest,
GetFocusedResponse, GetForeignToplevelListIdentifierRequest,
GetForeignToplevelListIdentifierResponse, GetLayoutModeRequest, GetLayoutModeResponse,
GetLocRequest, GetLocResponse, GetRequest, GetResponse, GetSizeRequest,
GetSizeResponse, GetTagIdsRequest, GetTagIdsResponse, GetTitleRequest,
GetTitleResponse, GetWindowsInDirRequest, GetWindowsInDirResponse, LowerRequest,
LowerResponse, MoveGrabRequest, MoveToOutputRequest, MoveToOutputResponse,
MoveToTagRequest, RaiseRequest, ResizeGrabRequest, ResizeTileRequest,
SetDecorationModeRequest, SetFloatingRequest, SetFocusedRequest, SetFullscreenRequest,
SetGeometryRequest, SetMaximizedRequest, SetTagRequest, SetTagsRequest,
SetTagsResponse, SetVrrDemandRequest, SetVrrDemandResponse, SwapRequest, SwapResponse,
WindowRuleRequest, WindowRuleResponse,
GetLocRequest, GetLocResponse, GetMinimizedRequest, GetMinimizedResponse, GetRequest,
GetResponse, GetSizeRequest, GetSizeResponse, GetTagIdsRequest, GetTagIdsResponse,
GetTitleRequest, GetTitleResponse, GetWindowsInDirRequest, GetWindowsInDirResponse,
LowerRequest, LowerResponse, MoveGrabRequest, MoveToOutputRequest,
MoveToOutputResponse, MoveToTagRequest, RaiseRequest, ResizeGrabRequest,
ResizeTileRequest, SetDecorationModeRequest, SetFloatingRequest, SetFocusedRequest,
SetFullscreenRequest, SetGeometryRequest, SetMaximizedRequest, SetMinimizedRequest,
SetTagRequest, SetTagsRequest, SetTagsResponse, SetVrrDemandRequest,
SetVrrDemandResponse, SwapRequest, SwapResponse, WindowRuleRequest, WindowRuleResponse,
},
},
};
Expand Down Expand Up @@ -194,6 +194,27 @@ impl v1::window_service_server::WindowService for super::WindowService {
.await
}

async fn get_minimized(
&self,
request: Request<GetMinimizedRequest>,
) -> TonicResult<GetMinimizedResponse> {
let window_id = WindowId(request.into_inner().window_id);

run_unary(&self.sender, move |state| {
let minimized = window_id
.window(&state.pinnacle)
.or_else(|| {
window_id
.unmapped_window(&state.pinnacle)
.map(|unmapped| unmapped.window.clone())
})
.map(|win| win.with_state(|state| state.minimized))
.unwrap_or(false);
Ok(GetMinimizedResponse { minimized })
})
.await
}

async fn get_tag_ids(
&self,
request: Request<GetTagIdsRequest>,
Expand Down Expand Up @@ -479,6 +500,31 @@ impl v1::window_service_server::WindowService for super::WindowService {
.await
}

async fn set_minimized(&self, request: Request<SetMinimizedRequest>) -> TonicResult<()> {
let request = request.into_inner();
let window_id = WindowId(request.window_id);
let absolute_minimized = match request.set_or_toggle() {
SetOrToggle::Unspecified => {
return Err(Status::invalid_argument("unspecified set or toggle"));
}
SetOrToggle::Set => Some(true),
SetOrToggle::Unset => Some(false),
SetOrToggle::Toggle => None,
};

run_unary_no_response(&self.sender, move |state| {
if let Some(window) = window_id.window(&state.pinnacle) {
crate::api::window::set_minimized(state, &window, absolute_minimized);
} else if let Some(unmapped) = window_id.unmapped_window_mut(&mut state.pinnacle)
&& let UnmappedState::WaitingForRules { rules: _, .. } = &mut unmapped.state
{
// TODO: find a way to immediately minimize a window upon mapping.
warn!("minimizing unmapped windows not yet supported");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does adding minimized to the WindowRules here and applying that here work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem to, with my preliminary testing.

However during testing I discovered that getting the focus stack of active tags no longer includes minimised windows, since they seem to lose their location value (which makes sense, but I'm not certain what exactly to do about it). During testing I made a custom config that retrieved all windows then filtered them for windows on the focused output and also minimization, but it is less convenient (but still doable if you combined it with some sort of active tag filter).

}
})
.await
}

async fn set_floating(&self, request: Request<SetFloatingRequest>) -> TonicResult<()> {
let request = request.into_inner();

Expand Down
3 changes: 2 additions & 1 deletion src/focus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ impl WindowKeyboardFocusStack {
/// Gets the currently focused window on this stack.
///
/// This is the topmost window that is on an active tag and not
/// an OR window.
/// an OR window, and is not minimized.
pub fn current_focus(&self) -> Option<&WindowElement> {
if !self.focused {
return None;
Expand All @@ -375,6 +375,7 @@ impl WindowKeyboardFocusStack {
.iter()
.rev()
.filter(|win| win.is_on_active_tag())
.filter(|win| win.with_state(|state| !state.minimized))
.find(|win| !win.is_x11_override_redirect())
}
}
10 changes: 8 additions & 2 deletions src/handlers/foreign_toplevel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ impl ForeignToplevelHandler for State {
return;
};

// TODO make a nice `self.pinnacle` function somewhere for this?? IDK though >.<
let was_minimized = window.with_state(|state| state.minimized);
window.with_state_mut(|state| state.minimized = false);
self.pinnacle.keyboard_focus_stack.set_focus(window.clone());
self.pinnacle.raise_window(window.clone());

Expand All @@ -37,6 +40,11 @@ impl ForeignToplevelHandler for State {
crate::api::tag::switch_to(self, &tag);
}
} else {
// Need to re-layout things if the window was un-minimized.
if was_minimized {
self.pinnacle.update_xwayland_stacking_order();
self.pinnacle.request_layout(&output);
}
self.schedule_render(&output);
}
}
Expand Down Expand Up @@ -103,7 +111,6 @@ impl ForeignToplevelHandler for State {
});
}

// TODO:
fn set_minimized(&mut self, wl_surface: WlSurface) {
let _span = tracy_client::span!("ForeignToplevelHandler::set_minimized");

Expand All @@ -121,7 +128,6 @@ impl ForeignToplevelHandler for State {
self.schedule_render(&output);
}

// TODO:
fn unset_minimized(&mut self, wl_surface: WlSurface) {
let _span = tracy_client::span!("ForeignToplevelHandler::unset_minimized");

Expand Down
4 changes: 3 additions & 1 deletion src/handlers/xwayland.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,9 @@ impl Pinnacle {
.iter()
.filter_map(|z| z.window())
.filter(|win| !win.is_x11_override_redirect())
.partition::<Vec<_>, _>(|win| win.is_on_active_tag());
.partition::<Vec<_>, _>(|win| {
win.is_on_active_tag() && win.with_state(|state| !state.minimized)
});

let active_windows = active_windows.into_iter().flat_map(|win| win.x11_surface());
let non_active_windows = non_active_windows
Expand Down
12 changes: 10 additions & 2 deletions src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ impl Pinnacle {
.filter(|win| win.output(self).as_ref() == Some(output))
.cloned()
.partition::<Vec<_>, _>(|win| {
win.with_state(|state| state.tags.intersection(&focused_tags).next().is_some())
win.with_state(|state| {
state.tags.intersection(&focused_tags).next().is_some() && !state.minimized
})
})
});

Expand Down Expand Up @@ -448,7 +450,12 @@ impl State {
for win in self.pinnacle.windows.iter() {
let is_tiled = win.with_state(|state| state.layout_mode.is_tiled());
let is_on_active_tag = win.is_on_active_tag();
if !is_tiled && is_on_active_tag && !self.pinnacle.space.elements().any(|w| w == win) {
let is_not_minimized = win.with_state(|state| !state.minimized);
if !is_tiled
&& is_not_minimized
&& is_on_active_tag
&& !self.pinnacle.space.elements().any(|w| w == win)
{
wins_to_update.push(win.clone());
}
}
Expand Down Expand Up @@ -479,6 +486,7 @@ impl Pinnacle {
.filter(|win| {
win.with_state(|state| state.tags.intersection(&focused_tags).next().is_some())
})
.filter(|win| win.with_state(|state| !state.minimized))
.cloned()
.collect::<Vec<_>>()
});
Expand Down
1 change: 1 addition & 0 deletions src/window/window_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ pub struct WindowElementState {
pub tags: IndexSet<Tag>,
pub layout_mode: LayoutMode,
pub old_layout_mode: Option<LayoutMode>,
/// Whether the window is minimised.
pub minimized: bool,
pub decoration_mode: Option<zxdg_toplevel_decoration_v1::Mode>,
pub floating_x: Option<i32>,
Expand Down
Loading