Skip to content
Open
Show file tree
Hide file tree
Changes from 18 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
40 changes: 38 additions & 2 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,16 +123,39 @@ 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 SetMinimizedResponse {}

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

message SetFocusedRequest {
// TrySetFocusedRequest is for the new RPC.
option deprecated = true;
uint32 window_id = 1;
pinnacle.util.v1.SetOrToggle set_or_toggle = 2;
}

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

message TrySetFocusedResponse {
enum TrySetFocusedStatus {
TRY_SET_FOCUSED_STATUS_SUCCESS = 0;
TRY_SET_FOCUSED_STATUS_WINDOW_MINIMIZED = 1;
}

TrySetFocusedStatus status = 1;
}

enum DecorationMode {
DECORATION_MODE_UNSPECIFIED = 0;
DECORATION_MODE_CLIENT_SIDE = 1;
Expand Down Expand Up @@ -222,6 +252,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,8 +262,13 @@ service WindowService {
rpc ResizeTile(ResizeTileRequest) returns (google.protobuf.Empty);
rpc SetFullscreen(SetFullscreenRequest) returns (google.protobuf.Empty);
rpc SetMaximized(SetMaximizedRequest) returns (google.protobuf.Empty);
rpc SetFloating(SetFloatingRequest) returns (google.protobuf.Empty);
rpc SetFocused(SetFocusedRequest) returns (google.protobuf.Empty);
rpc SetMinimized(SetMinimizedRequest) returns (SetMinimizedResponse);
rpc SetFloating(SetFloatingRequest) returns (google.protobuf.Empty);
rpc SetFocused(SetFocusedRequest) returns (google.protobuf.Empty) {
// use `TrySetFocused` instead.
option deprecated = true;
};
rpc TrySetFocused(TrySetFocusedRequest) returns (TrySetFocusedResponse);
rpc SetDecorationMode(SetDecorationModeRequest) returns (google.protobuf.Empty);
rpc MoveToTag(MoveToTagRequest) returns (google.protobuf.Empty);
rpc SetTag(SetTagRequest) returns (google.protobuf.Empty);
Expand Down
29 changes: 28 additions & 1 deletion api/rust/examples/default_config/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,33 @@ async fn config() {
.group("Window")
.description("Toggle maximized on the focused window");

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

// `mon_key + shift + n` unminimises the "most recently focused"
// minimised window that is on the active tags in this current output.
input::keybind(mod_key | Mod::SHIFT, 'n')
.on_press(|| {
let Some(output) = output::get_focused() else { return };
let Some(most_recently_minimised_active_window) = output
.keyboard_focus_stack_visible()
.filter(|w| w.minimized())
.last()
Comment on lines +170 to +172

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.

Note on recent modification: The name of the API function "keyboard_focus_stack_visible" seems misleading now that minimised windows are a thing >.<

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.

Hm guess we can deprecate it and add a renamed function

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.

Some sort of change seems to have made minimized windows not show up in it anyway (as they now lack a location), which makes more sense for the name anyway IMO (as they're not visible :p). But this also breaks the focus stack as a more general tool since it filters out windows by location down in the element check and minimization deletes that for some reason. Or at the very least the minimized windows don't get listed for their associated outputs and I think it's for that reason based on some testing >.<...

else {
return;
};
most_recently_minimised_active_window.set_minimized(false);
})
.group("Window")
.description("Unminimize the most recently focused window on the active tags");

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

input::keybind(Mod::empty(), Keysym::XF86_AudioRaiseVolume)
Expand Down Expand Up @@ -462,7 +489,7 @@ async fn config() {

// Enable sloppy focus
window::connect_signal(WindowSignal::PointerEnter(Box::new(|win| {
win.set_focused(true);
let _ = win.try_set_focused(true);
})));

// Focus outputs when the pointer enters them
Expand Down
23 changes: 23 additions & 0 deletions api/rust/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,26 @@ impl From<Rect> for pinnacle_api_defs::pinnacle::util::v1::Rect {
}
}
}

/// Extension trait for [`Result`].
pub(crate) trait ResultExt {
type T;
type E;

/// Swap the "error" and "ok" components of a result.
fn swap_ok_err(self) -> Result<Self::E, Self::T>;
}

impl<T, E> ResultExt for Result<T, E> {
type T = T;

type E = E;

#[inline(always)]
fn swap_ok_err(self) -> Result<Self::E, Self::T> {
match self {
Ok(t) => Err(t),
Err(e) => Ok(e),
}
}
}
115 changes: 104 additions & 11 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,
SetFullscreenRequest, SetGeometryRequest, SetMaximizedRequest, SetMinimizedRequest,
SetTagRequest, SetTagsRequest, SetVrrDemandRequest, SwapRequest, TrySetFocusedRequest,
},
},
};
Expand All @@ -38,7 +38,7 @@ use crate::{
output::OutputHandle,
signal::{SignalHandle, WindowSignal},
tag::TagHandle,
util::{Batch, Direction, Point, Size},
util::{Batch, Direction, Point, ResultExt, Size},
};

/// Gets handles to all windows.
Expand Down Expand Up @@ -235,6 +235,29 @@ impl VrrDemand {
}
}

/// Error when trying to focus/unfocus a window.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TrySetFocusedError {
/// Window was minimized and could not be focused.
WindowMinimized,
}

impl TryFrom<pinnacle_api_defs::pinnacle::window::v1::TrySetFocusedResponse>
for TrySetFocusedError
{
type Error = ();

fn try_from(
value: pinnacle_api_defs::pinnacle::window::v1::TrySetFocusedResponse,
) -> Result<Self, Self::Error> {
use pinnacle_api_defs::pinnacle::window::v1::try_set_focused_response::TrySetFocusedStatus;
match value.status() {
TrySetFocusedStatus::Success => Err(()),
TrySetFocusedStatus::WindowMinimized => Ok(Self::WindowMinimized),
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just my 2c, but this feels weird (as does the whole swap_ok_err() extension method).

Is there a reason not to implement From<TrySetFocuseResponse> for Result<(), TrySetFocusedError> instead ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since it's just a gut-feeling, maybe don't act on it until Ottatop give an input :)

If the issue is that rust don't like when we implements foreign traits on foreign types, it was solved on the server-side by having a FromApi trait, with the same semantics as From, and I feel it make the code a bit cleaner

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.

Ahhh... I probably should have done that instead of TryFrom, yes. There was a reason (when I had TrySetFocusedError as part of the protobuf generated rust files, I couldn't implement the trait due to orphan rules) but it shouldn't be an issue now ^.^

@sapient-cogbag sapient-cogbag Feb 11, 2026

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.

Actually, scratch that, it still doesn't work. I could try implementing Into instead, but that's really not ideal nya.

Edit: this also doesn't work. Didn't think it would but just wanted to check. The problem is that both Result and the TrySetFocusResponse type are foreign to the main pinnacle crate.

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.

We could also create a separate TryFrom/TryInto-like pair of traits instead for responses with embedded errors and use that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actually, scratch that, it still doesn't work. I could try implementing Into instead, but that's really not ideal nya.

Edit: this also doesn't work. Didn't think it would but just wanted to check. The problem is that both Result and the TrySetFocusResponse type are foreign to the main pinnacle crate.

I'm not sure what you tried, but the orphan rule shouldn't apply if the trait is local to the crate (i.e. if you copy https://github.com/pinnacle-comp/pinnacle/blob/main/snowcap/src/util/convert.rs to from server-side to the config api & use FromAPI instead of From)

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.

Yes. I was using the "default" (Try)Into/(Try)From traits from stdlib. It would work if I used a trait local to the crate. I will do this.


impl WindowHandle {
/// Sends a close request to this window.
///
Expand Down Expand Up @@ -365,6 +388,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 @@ -398,10 +449,20 @@ impl WindowHandle {
}

/// Focuses or unfocuses this window.
///
/// Silently fails if trying to focus a minimized window.
#[deprecated = "use `WindowHandle::try_set_focused` instead"]
pub fn set_focused(&self, set: bool) {
let _ = self.try_set_focused(set);
}

/// Tries to focus or unfocus this window.
///
/// Fails if the window is minimized.
pub fn try_set_focused(&self, set: bool) -> Result<(), TrySetFocusedError> {
let window_id = self.id;
Client::window()
.set_focused(SetFocusedRequest {
.try_set_focused(TrySetFocusedRequest {
window_id,
set_or_toggle: match set {
true => SetOrToggle::Set,
Expand All @@ -410,19 +471,35 @@ impl WindowHandle {
.into(),
})
.block_on_tokio()
.unwrap();
.expect("successful rpc communication is expected")
.into_inner()
.try_into()
.swap_ok_err()
}

/// Toggles this window between focused and unfocused.
///
/// Silently fails if trying to focus a minimized window.
#[deprecated = "use `WindowHandle::try_toggle_focused` instead"]
pub fn toggle_focused(&self) {
let _ = self.try_toggle_focused();
}

/// Tries to toggle this window between focused and unfocused.
///
/// Fails if the window is minimized.
pub fn try_toggle_focused(&self) -> Result<(), TrySetFocusedError> {
let window_id = self.id;
Client::window()
.set_focused(SetFocusedRequest {
.try_set_focused(TrySetFocusedRequest {
window_id,
set_or_toggle: SetOrToggle::Toggle.into(),
})
.block_on_tokio()
.unwrap();
.expect("successful rpc communication is expected")
.into_inner()
.try_into()
.swap_ok_err()
}

/// Sets this window's decoration mode.
Expand Down Expand Up @@ -807,6 +884,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
Loading