pinnacle_api/window.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Window management.
6//!
7//! This module provides ways to get [`WindowHandle`]s and move and resize
8//! windows using the mouse.
9//!
10//! [`WindowHandle`]s allow you to do things like resize and move windows, toggle them between
11//! floating and tiled, close them, and more.
12
13use std::borrow::Borrow;
14
15use futures::FutureExt;
16use pinnacle_api_defs::pinnacle::{
17 util::v1::SetOrToggle,
18 window::{
19 self,
20 v1::{
21 GetAppIdRequest, GetFocusedRequest, GetForeignToplevelListIdentifierRequest,
22 GetLayoutModeRequest, GetLocRequest, GetSizeRequest, GetTagIdsRequest, GetTitleRequest,
23 GetWindowsInDirRequest, LowerRequest, MoveGrabRequest, MoveToOutputRequest,
24 MoveToTagRequest, RaiseRequest, ResizeGrabRequest, ResizeTileRequest,
25 SetDecorationModeRequest, SetFloatingRequest, SetFocusedRequest, SetFullscreenRequest,
26 SetGeometryRequest, SetMaximizedRequest, SetTagRequest, SetTagsRequest,
27 SetVrrDemandRequest, SwapRequest,
28 },
29 },
30};
31use tokio::sync::mpsc::unbounded_channel;
32use tokio_stream::StreamExt;
33
34use crate::{
35 BlockOnTokio,
36 client::Client,
37 input::MouseButton,
38 output::OutputHandle,
39 signal::{SignalHandle, WindowSignal},
40 tag::TagHandle,
41 util::{Batch, Direction, Point, Size},
42};
43
44/// Gets handles to all windows.
45///
46/// # Examples
47///
48/// ```no_run
49/// # use pinnacle_api::window;
50/// for win in window::get_all() {
51/// println!("{}", win.title());
52/// }
53/// ```
54pub fn get_all() -> impl Iterator<Item = WindowHandle> {
55 get_all_async().block_on_tokio()
56}
57
58/// Async impl for [`get_all`].
59pub async fn get_all_async() -> impl Iterator<Item = WindowHandle> {
60 let window_ids = Client::window()
61 .get(pinnacle_api_defs::pinnacle::window::v1::GetRequest {})
62 .await
63 .unwrap()
64 .into_inner()
65 .window_ids;
66
67 window_ids.into_iter().map(|id| WindowHandle { id })
68}
69
70/// Gets a handle to the window with the current keyboard focus.
71///
72/// # Examples
73///
74/// ```no_run
75/// # use pinnacle_api::window;
76/// if let Some(focused) = window::get_focused() {
77/// println!("{}", focused.title());
78/// }
79/// ```
80pub fn get_focused() -> Option<WindowHandle> {
81 get_focused_async().block_on_tokio()
82}
83
84/// Async impl for [`get_focused`].
85pub async fn get_focused_async() -> Option<WindowHandle> {
86 let windows = get_all_async().await;
87
88 windows.batch_find(|win| win.focused_async().boxed(), |focused| *focused)
89}
90
91/// Begins an interactive window move.
92///
93/// This will start moving the window under the pointer until `button` is released.
94///
95/// `button` should be the mouse button that is held at the time
96/// this function is called. Otherwise, the move will not start.
97/// This is intended for use in tandem with a mousebind.
98///
99/// # Examples
100///
101/// ```no_run
102/// # use pinnacle_api::window;
103/// # use pinnacle_api::input;
104/// # use pinnacle_api::input::Mod;
105/// # use pinnacle_api::input::MouseButton;
106/// input::mousebind(Mod::SUPER, MouseButton::Left)
107/// .on_press(|| window::begin_move(MouseButton::Left));
108/// ```
109pub fn begin_move(button: MouseButton) {
110 Client::window()
111 .move_grab(MoveGrabRequest {
112 button: button.into(),
113 })
114 .block_on_tokio()
115 .unwrap();
116}
117
118/// Begins an interactive window resize.
119///
120/// This will start resizing the window under the pointer until `button` is released.
121///
122/// `button` should be the mouse button that is held at the time
123/// this function is called. Otherwise, the move will not start.
124/// This is intended for use in tandem with a mousebind.
125///
126/// # Examples
127///
128/// ```no_run
129/// # use pinnacle_api::window;
130/// # use pinnacle_api::input;
131/// # use pinnacle_api::input::Mod;
132/// # use pinnacle_api::input::MouseButton;
133/// input::mousebind(Mod::SUPER, MouseButton::Right)
134/// .on_press(|| window::begin_resize(MouseButton::Right));
135/// ```
136pub fn begin_resize(button: MouseButton) {
137 Client::window()
138 .resize_grab(ResizeGrabRequest {
139 button: button.into(),
140 })
141 .block_on_tokio()
142 .unwrap();
143}
144
145/// Connects to a [`WindowSignal`].
146///
147/// # Examples
148///
149/// ```no_run
150/// # use pinnacle_api::window;
151/// # use pinnacle_api::signal::WindowSignal;
152/// window::connect_signal(WindowSignal::PointerEnter(Box::new(|window| {
153/// window.set_focused(true);
154/// })));
155/// ```
156pub fn connect_signal(signal: WindowSignal) -> SignalHandle {
157 let mut signal_state = Client::signal_state();
158
159 match signal {
160 WindowSignal::PointerEnter(f) => signal_state.window_pointer_enter.add_callback(f),
161 WindowSignal::PointerLeave(f) => signal_state.window_pointer_leave.add_callback(f),
162 WindowSignal::Focused(f) => signal_state.window_focused.add_callback(f),
163 WindowSignal::TitleChanged(f) => signal_state.window_title_changed.add_callback(f),
164 WindowSignal::LayoutModeChanged(f) => {
165 signal_state.window_layout_mode_changed.add_callback(f)
166 }
167 WindowSignal::Created(f) => signal_state.window_created.add_callback(f),
168 WindowSignal::Destroyed(f) => signal_state.window_destroyed.add_callback(f),
169 }
170}
171
172/// A handle to a window.
173///
174/// This allows you to manipulate the window and get its properties.
175#[derive(Debug, Clone, PartialEq, Eq, Hash)]
176pub struct WindowHandle {
177 pub(crate) id: u32,
178}
179
180/// A window's current layout mode.
181#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
182pub enum LayoutMode {
183 /// The window is tiled.
184 Tiled,
185 /// The window is floating.
186 Floating,
187 /// The window is fullscreen.
188 Fullscreen,
189 /// The window is maximized.
190 Maximized,
191}
192
193impl TryFrom<window::v1::LayoutMode> for LayoutMode {
194 type Error = ();
195
196 fn try_from(value: window::v1::LayoutMode) -> Result<Self, Self::Error> {
197 match value {
198 window::v1::LayoutMode::Unspecified => Err(()),
199 window::v1::LayoutMode::Tiled => Ok(LayoutMode::Tiled),
200 window::v1::LayoutMode::Floating => Ok(LayoutMode::Floating),
201 window::v1::LayoutMode::Fullscreen => Ok(LayoutMode::Fullscreen),
202 window::v1::LayoutMode::Maximized => Ok(LayoutMode::Maximized),
203 // window::v1::LayoutMode::Spilled => Ok(LayoutMode::Floating),
204 }
205 }
206}
207
208/// A mode for window decorations (titlebar, shadows, etc).
209#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
210pub enum DecorationMode {
211 /// The client should draw its own decorations.
212 ClientSide,
213 /// The server should draw decorations.
214 ServerSide,
215}
216
217/// A demand for variable refresh rate on an output.
218#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq)]
219#[non_exhaustive]
220pub struct VrrDemand {
221 /// Whether the window must be fullscreen for vrr to turn on.
222 pub fullscreen: bool,
223}
224
225impl VrrDemand {
226 /// Creates a [`VrrDemand`] that turns on vrr when a window is visible.
227 pub fn when_visible() -> Self {
228 Default::default()
229 }
230
231 /// Creates a [`VrrDemand`] that turns on vrr when a window is both
232 /// visible *and* fullscreen.
233 pub fn when_fullscreen() -> Self {
234 Self { fullscreen: true }
235 }
236}
237
238impl WindowHandle {
239 /// Sends a close request to this window.
240 ///
241 /// If the window is unresponsive, it may not close.
242 pub fn close(&self) {
243 let window_id = self.id;
244 Client::window()
245 .close(pinnacle_api_defs::pinnacle::window::v1::CloseRequest { window_id })
246 .block_on_tokio()
247 .unwrap();
248 }
249
250 /// Sets this window's location and/or size.
251 ///
252 /// Only affects the floating geometry of windows. Tiled geometries are calculated
253 /// using the current layout.
254 pub fn set_geometry(
255 &self,
256 x: impl Into<Option<i32>>,
257 y: impl Into<Option<i32>>,
258 w: impl Into<Option<u32>>,
259 h: impl Into<Option<u32>>,
260 ) {
261 Client::window()
262 .set_geometry(SetGeometryRequest {
263 window_id: self.id,
264 x: x.into(),
265 y: y.into(),
266 w: w.into(),
267 h: h.into(),
268 })
269 .block_on_tokio()
270 .unwrap();
271 }
272
273 /// If this window is tiled, resizes its tile by shifting the left, right,
274 /// top, and bottom edges by the provided pixel amounts.
275 ///
276 /// Positive amounts shift edges right/down, while negative amounts
277 /// shift edges left/up.
278 ///
279 /// If this resizes the tile in a direction that it can no longer resize
280 /// towards (e.g. it's at the edge of the screen), it will resize in the opposite
281 /// direction.
282 ///
283 /// # Examples
284 ///
285 /// ```no_run
286 /// # use pinnacle_api::window;
287 /// # || {
288 /// // Grow the focused tiled window 10 pixels leftward
289 /// window::get_focused()?.resize_tile(-10, 0, 0, 0);
290 ///
291 /// // Shrink the focused tiled window 10 pixels inward from the right
292 /// window::get_focused()?.resize_tile(0, -10, 0, 0);
293 ///
294 /// // Grow the focused tiled window 20 pixels centered vertically
295 /// window::get_focused()?.resize_tile(0, 0, -10, 10);
296 /// # Some(())
297 /// # };
298 /// ```
299 pub fn resize_tile(&self, left: i32, right: i32, top: i32, bottom: i32) {
300 Client::window()
301 .resize_tile(ResizeTileRequest {
302 window_id: self.id,
303 left,
304 right,
305 top,
306 bottom,
307 })
308 .block_on_tokio()
309 .unwrap();
310 }
311
312 /// Sets this window to fullscreen or not.
313 pub fn set_fullscreen(&self, set: bool) {
314 let window_id = self.id;
315 Client::window()
316 .set_fullscreen(SetFullscreenRequest {
317 window_id,
318 set_or_toggle: match set {
319 true => SetOrToggle::Set,
320 false => SetOrToggle::Unset,
321 }
322 .into(),
323 })
324 .block_on_tokio()
325 .unwrap();
326 }
327
328 /// Toggles this window between fullscreen and not.
329 pub fn toggle_fullscreen(&self) {
330 let window_id = self.id;
331 Client::window()
332 .set_fullscreen(SetFullscreenRequest {
333 window_id,
334 set_or_toggle: SetOrToggle::Toggle.into(),
335 })
336 .block_on_tokio()
337 .unwrap();
338 }
339
340 /// Sets this window to maximized or not.
341 pub fn set_maximized(&self, set: bool) {
342 let window_id = self.id;
343 Client::window()
344 .set_maximized(SetMaximizedRequest {
345 window_id,
346 set_or_toggle: match set {
347 true => SetOrToggle::Set,
348 false => SetOrToggle::Unset,
349 }
350 .into(),
351 })
352 .block_on_tokio()
353 .unwrap();
354 }
355
356 /// Toggles this window between maximized and not.
357 pub fn toggle_maximized(&self) {
358 let window_id = self.id;
359 Client::window()
360 .set_maximized(SetMaximizedRequest {
361 window_id,
362 set_or_toggle: SetOrToggle::Toggle.into(),
363 })
364 .block_on_tokio()
365 .unwrap();
366 }
367
368 /// Sets this window to floating or not.
369 ///
370 /// Floating windows will not be tiled and can be moved around and resized freely.
371 pub fn set_floating(&self, set: bool) {
372 let window_id = self.id;
373 Client::window()
374 .set_floating(SetFloatingRequest {
375 window_id,
376 set_or_toggle: match set {
377 true => SetOrToggle::Set,
378 false => SetOrToggle::Unset,
379 }
380 .into(),
381 })
382 .block_on_tokio()
383 .unwrap();
384 }
385
386 /// Toggles this window to and from floating.
387 ///
388 /// Floating windows will not be tiled and can be moved around and resized freely.
389 pub fn toggle_floating(&self) {
390 let window_id = self.id;
391 Client::window()
392 .set_floating(SetFloatingRequest {
393 window_id,
394 set_or_toggle: SetOrToggle::Toggle.into(),
395 })
396 .block_on_tokio()
397 .unwrap();
398 }
399
400 /// Focuses or unfocuses this window.
401 pub fn set_focused(&self, set: bool) {
402 let window_id = self.id;
403 Client::window()
404 .set_focused(SetFocusedRequest {
405 window_id,
406 set_or_toggle: match set {
407 true => SetOrToggle::Set,
408 false => SetOrToggle::Unset,
409 }
410 .into(),
411 })
412 .block_on_tokio()
413 .unwrap();
414 }
415
416 /// Toggles this window between focused and unfocused.
417 pub fn toggle_focused(&self) {
418 let window_id = self.id;
419 Client::window()
420 .set_focused(SetFocusedRequest {
421 window_id,
422 set_or_toggle: SetOrToggle::Toggle.into(),
423 })
424 .block_on_tokio()
425 .unwrap();
426 }
427
428 /// Sets this window's decoration mode.
429 pub fn set_decoration_mode(&self, mode: DecorationMode) {
430 Client::window()
431 .set_decoration_mode(SetDecorationModeRequest {
432 window_id: self.id,
433 decoration_mode: match mode {
434 DecorationMode::ClientSide => window::v1::DecorationMode::ClientSide,
435 DecorationMode::ServerSide => window::v1::DecorationMode::ServerSide,
436 }
437 .into(),
438 })
439 .block_on_tokio()
440 .unwrap();
441 }
442
443 /// Moves this window to the specified output.
444 ///
445 /// This will set the window tags to the output tags, and update the window position.
446 ///
447 /// # Example
448 ///
449 /// ```no_run
450 /// # use pinnacle_api::window;
451 /// # use pinnacle_api::output;
452 /// # || {
453 /// // Move the focused window to output DP-2
454 /// window::get_focused()?.move_to_output(&output::get_by_name("DP-2")?);
455 /// # Some(())
456 /// # };
457 /// ```
458 pub fn move_to_output(&self, output: &OutputHandle) {
459 let window_id = self.id;
460 let output_name = output.name();
461
462 Client::window()
463 .move_to_output(MoveToOutputRequest {
464 window_id,
465 output_name,
466 })
467 .block_on_tokio()
468 .unwrap();
469 }
470
471 /// Moves this window to the given `tag`.
472 ///
473 /// This will remove all tags from this window then tag it with `tag`, essentially moving the
474 /// window to that tag.
475 ///
476 /// # Examples
477 ///
478 /// ```no_run
479 /// # use pinnacle_api::window;
480 /// # use pinnacle_api::tag;
481 /// # || {
482 /// // Move the focused window to tag "Code" on the focused output
483 /// window::get_focused()?.move_to_tag(&tag::get("Code")?);
484 /// # Some(())
485 /// # };
486 /// ```
487 pub fn move_to_tag(&self, tag: &TagHandle) {
488 let window_id = self.id;
489 let tag_id = tag.id;
490 Client::window()
491 .move_to_tag(MoveToTagRequest { window_id, tag_id })
492 .block_on_tokio()
493 .unwrap();
494 }
495
496 /// Sets or unsets a tag on this window.
497 ///
498 /// # Examples
499 ///
500 /// ```no_run
501 /// # use pinnacle_api::window;
502 /// # use pinnacle_api::tag;
503 /// # || {
504 /// let focused = window::get_focused()?;
505 /// let tag = tag::get("Potato")?;
506 ///
507 /// focused.set_tag(&tag, true); // `focused` now has tag "Potato"
508 /// focused.set_tag(&tag, false); // `focused` no longer has tag "Potato"
509 /// # Some(())
510 /// # };
511 /// ```
512 pub fn set_tag(&self, tag: &TagHandle, set: bool) {
513 let window_id = self.id;
514 let tag_id = tag.id;
515 Client::window()
516 .set_tag(SetTagRequest {
517 window_id,
518 tag_id,
519 set_or_toggle: match set {
520 true => SetOrToggle::Set,
521 false => SetOrToggle::Unset,
522 }
523 .into(),
524 })
525 .block_on_tokio()
526 .unwrap();
527 }
528
529 /// Toggles a tag on this window.
530 ///
531 /// # Examples
532 ///
533 /// ```no_run
534 /// # use pinnacle_api::window;
535 /// # use pinnacle_api::tag;
536 /// # || {
537 /// let focused = window::get_focused()?;
538 /// let tag = tag::get("Potato")?;
539 ///
540 /// focused.toggle_tag(&tag); // `focused` now has tag "Potato"
541 /// focused.toggle_tag(&tag); // `focused` no longer has tag "Potato"
542 /// # Some(())
543 /// # };
544 /// ```
545 pub fn toggle_tag(&self, tag: &TagHandle) {
546 let window_id = self.id;
547 let tag_id = tag.id;
548 Client::window()
549 .set_tag(SetTagRequest {
550 window_id,
551 tag_id,
552 set_or_toggle: SetOrToggle::Toggle.into(),
553 })
554 .block_on_tokio()
555 .unwrap();
556 }
557
558 /// Sets the exact provided tags on this window.
559 ///
560 /// Passing in an empty collection will not change the window's tags.
561 ///
562 /// For ergonomics, this accepts iterators of both `TagHandle` and `&TagHandle`.
563 ///
564 /// # Examples
565 ///
566 /// ```no_run
567 /// # use pinnacle_api::window;
568 /// # use pinnacle_api::tag;
569 /// # || {
570 /// let focused = window::get_focused()?;
571 /// let tag1 = tag::get("1")?;
572 /// let tag3 = tag::get("3")?;
573 ///
574 /// // Set `focused`'s tags to "1" and "3", removing all others
575 /// focused.set_tags([tag1, tag3]);
576 /// # Some(())
577 /// # };
578 /// ```
579 pub fn set_tags<T: Borrow<TagHandle>>(&self, tags: impl IntoIterator<Item = T>) {
580 let window_id = self.id;
581 let tag_ids = tags
582 .into_iter()
583 .map(|tag| {
584 let tag: &TagHandle = tag.borrow();
585 tag.id
586 })
587 .collect::<Vec<_>>();
588
589 Client::window()
590 .set_tags(SetTagsRequest { window_id, tag_ids })
591 .block_on_tokio()
592 .unwrap();
593 }
594
595 /// Sets this window's [`VrrDemand`].
596 ///
597 /// When set to `None`, this window has no vrr demand.
598 ///
599 /// This works in conjunction with an output with
600 /// [`Vrr::OnDemand`](crate::output::Vrr::OnDemand).
601 pub fn set_vrr_demand(&self, vrr_demand: impl Into<Option<VrrDemand>>) {
602 let window_id = self.id;
603 let vrr_demand: Option<_> = vrr_demand.into();
604
605 Client::window()
606 .set_vrr_demand(SetVrrDemandRequest {
607 window_id,
608 vrr_demand: vrr_demand.map(|vrr_demand| window::v1::VrrDemand {
609 fullscreen: vrr_demand.fullscreen,
610 }),
611 })
612 .block_on_tokio()
613 .unwrap();
614 }
615
616 /// Raises this window to the front.
617 pub fn raise(&self) {
618 let window_id = self.id;
619 Client::window()
620 .raise(RaiseRequest { window_id })
621 .block_on_tokio()
622 .unwrap();
623 }
624
625 /// Lowers this window to the back.
626 pub fn lower(&self) {
627 let window_id = self.id;
628 Client::window()
629 .lower(LowerRequest { window_id })
630 .block_on_tokio()
631 .unwrap();
632 }
633
634 /// Gets this window's current location in the global space.
635 pub fn loc(&self) -> Option<Point> {
636 self.loc_async().block_on_tokio()
637 }
638
639 /// Async impl for [`Self::loc`].
640 pub async fn loc_async(&self) -> Option<Point> {
641 let window_id = self.id;
642 Client::window()
643 .get_loc(GetLocRequest { window_id })
644 .await
645 .unwrap()
646 .into_inner()
647 .loc
648 .map(|loc| Point { x: loc.x, y: loc.y })
649 }
650
651 /// Gets this window's current size.
652 pub fn size(&self) -> Option<Size> {
653 self.size_async().block_on_tokio()
654 }
655
656 /// Async impl for [`Self::size`].
657 pub async fn size_async(&self) -> Option<Size> {
658 let window_id = self.id;
659 Client::window()
660 .get_size(GetSizeRequest { window_id })
661 .await
662 .unwrap()
663 .into_inner()
664 .size
665 .map(|size| Size {
666 w: size.width,
667 h: size.height,
668 })
669 }
670
671 /// Gets this window's app id (class if it's an xwayland window).
672 ///
673 /// If it doesn't have one, this returns an empty string.
674 pub fn app_id(&self) -> String {
675 self.app_id_async().block_on_tokio()
676 }
677
678 /// Async impl for [`Self::app_id`].
679 pub async fn app_id_async(&self) -> String {
680 let window_id = self.id;
681 Client::window()
682 .get_app_id(GetAppIdRequest { window_id })
683 .await
684 .unwrap()
685 .into_inner()
686 .app_id
687 }
688
689 /// Gets this window's title.
690 ///
691 /// If it doesn't have one, this returns an empty string.
692 pub fn title(&self) -> String {
693 self.title_async().block_on_tokio()
694 }
695
696 /// Async impl for [`Self::title`].
697 pub async fn title_async(&self) -> String {
698 let window_id = self.id;
699 Client::window()
700 .get_title(GetTitleRequest { window_id })
701 .await
702 .unwrap()
703 .into_inner()
704 .title
705 }
706
707 /// Gets this window's output.
708 ///
709 /// This is currently implemented as the output of the first
710 /// tag that this window has.
711 ///
712 /// Returns `None` if this window doesn't exist or if it has no tags.
713 pub fn output(&self) -> Option<OutputHandle> {
714 self.output_async().block_on_tokio()
715 }
716
717 /// Async impl for [`Self::output`].
718 pub async fn output_async(&self) -> Option<OutputHandle> {
719 Some(self.tags_async().await.next()?.output())
720 }
721
722 /// Gets whether or not this window has keyboard focus.
723 pub fn focused(&self) -> bool {
724 self.focused_async().block_on_tokio()
725 }
726
727 /// Async impl for [`Self::focused`].
728 pub async fn focused_async(&self) -> bool {
729 let window_id = self.id;
730 Client::window()
731 .get_focused(GetFocusedRequest { window_id })
732 .await
733 .unwrap()
734 .into_inner()
735 .focused
736 }
737
738 /// Gets this window's current [`LayoutMode`].
739 pub fn layout_mode(&self) -> LayoutMode {
740 self.layout_mode_async().block_on_tokio()
741 }
742
743 /// Async impl for [`Self::layout_mode`].
744 pub async fn layout_mode_async(&self) -> LayoutMode {
745 let window_id = self.id;
746 Client::window()
747 .get_layout_mode(GetLayoutModeRequest { window_id })
748 .await
749 .unwrap()
750 .into_inner()
751 .layout_mode()
752 .try_into()
753 .unwrap_or(LayoutMode::Tiled)
754 }
755
756 /// Gets whether or not this window is floating.
757 pub fn floating(&self) -> bool {
758 self.floating_async().block_on_tokio()
759 }
760
761 /// Async impl for [`Self::floating`].
762 pub async fn floating_async(&self) -> bool {
763 self.layout_mode_async().await == LayoutMode::Floating
764 }
765
766 /// Gets whether or not this window is tiled.
767 pub fn tiled(&self) -> bool {
768 self.tiled_async().block_on_tokio()
769 }
770
771 /// Async impl for [`Self::tiled`].
772 pub async fn tiled_async(&self) -> bool {
773 self.layout_mode_async().await == LayoutMode::Tiled
774 }
775
776 // /// Gets whether or not this window is spilled from the layout.
777 // ///
778 // /// A window is spilled when the current layout doesn't contains enough nodes and the
779 // /// compositor cannot assign a geometry to it. In that state, the window behaves as a floating
780 // /// window except that it gets tiled again if the number of nodes become big enough.
781 // pub fn spilled(&self) -> bool {
782 // self.spilled_async().block_on_tokio()
783 // }
784
785 // /// Async impl for [`Self::spilled`].
786 // pub async fn spilled_async(&self) -> bool {
787 // self.layout_mode_async().await == LayoutMode::Spilled
788 // }
789
790 /// Gets whether or not this window is fullscreen.
791 pub fn fullscreen(&self) -> bool {
792 self.fullscreen_async().block_on_tokio()
793 }
794
795 /// Async impl for [`Self::fullscreen`].
796 pub async fn fullscreen_async(&self) -> bool {
797 self.layout_mode_async().await == LayoutMode::Fullscreen
798 }
799
800 /// Gets whether or not this window is maximized.
801 pub fn maximized(&self) -> bool {
802 self.maximized_async().block_on_tokio()
803 }
804
805 /// Async impl for [`Self::maximized`].
806 pub async fn maximized_async(&self) -> bool {
807 self.layout_mode_async().await == LayoutMode::Maximized
808 }
809
810 /// Gets handles to all tags on this window.
811 pub fn tags(&self) -> impl Iterator<Item = TagHandle> + use<> {
812 self.tags_async().block_on_tokio()
813 }
814
815 /// Async impl for [`Self::tags`].
816 pub async fn tags_async(&self) -> impl Iterator<Item = TagHandle> + use<> {
817 let window_id = self.id;
818 Client::window()
819 .get_tag_ids(GetTagIdsRequest { window_id })
820 .await
821 .unwrap()
822 .into_inner()
823 .tag_ids
824 .into_iter()
825 .map(|id| TagHandle { id })
826 }
827
828 /// Gets whether or not this window has an active tag.
829 pub fn is_on_active_tag(&self) -> bool {
830 self.is_on_active_tag_async().block_on_tokio()
831 }
832
833 /// Async impl for [`Self::is_on_active_tag`].
834 pub async fn is_on_active_tag_async(&self) -> bool {
835 self.tags_async()
836 .await
837 .batch_find(|tag| tag.active_async().boxed(), |active| *active)
838 .is_some()
839 }
840
841 /// Gets all windows in the provided direction, sorted closest to farthest.
842 pub fn in_direction(&self, direction: Direction) -> impl Iterator<Item = WindowHandle> + use<> {
843 self.in_direction_async(direction).block_on_tokio()
844 }
845
846 /// Async impl for [`Self::in_direction`].
847 pub async fn in_direction_async(
848 &self,
849 direction: Direction,
850 ) -> impl Iterator<Item = WindowHandle> + use<> {
851 let window_id = self.id;
852
853 let mut request = GetWindowsInDirRequest {
854 window_id,
855 dir: Default::default(),
856 };
857
858 request.set_dir(match direction {
859 Direction::Left => pinnacle_api_defs::pinnacle::util::v1::Dir::Left,
860 Direction::Right => pinnacle_api_defs::pinnacle::util::v1::Dir::Right,
861 Direction::Up => pinnacle_api_defs::pinnacle::util::v1::Dir::Up,
862 Direction::Down => pinnacle_api_defs::pinnacle::util::v1::Dir::Down,
863 });
864
865 let response = Client::window()
866 .get_windows_in_dir(request)
867 .await
868 .unwrap()
869 .into_inner();
870
871 response.window_ids.into_iter().map(WindowHandle::from_id)
872 }
873
874 /// Gets this window's ext-foreign-toplevel-list handle identifier.
875 pub fn foreign_toplevel_list_identifier(&self) -> Option<String> {
876 self.foreign_toplevel_list_identifier_async()
877 .block_on_tokio()
878 }
879
880 /// Async impl for [`Self::foreign_toplevel_list_identifier`].
881 pub async fn foreign_toplevel_list_identifier_async(&self) -> Option<String> {
882 let window_id = self.id;
883
884 Client::window()
885 .get_foreign_toplevel_list_identifier(GetForeignToplevelListIdentifierRequest {
886 window_id,
887 })
888 .await
889 .unwrap()
890 .into_inner()
891 .identifier
892 }
893
894 /// Swap position with another window.
895 pub fn swap(&self, target: &WindowHandle) {
896 self.swap_async(target).block_on_tokio()
897 }
898
899 /// Async impl for [`Self::swap`].
900 pub async fn swap_async(&self, target: &WindowHandle) {
901 let request = SwapRequest {
902 window_id: self.id,
903 target_id: target.id,
904 };
905
906 Client::window().swap(request).await.unwrap();
907 }
908
909 /// Gets this window's raw compositor id.
910 pub fn id(&self) -> u32 {
911 self.id
912 }
913
914 /// Creates a window handle from an ID.
915 ///
916 /// Note: This is mostly for testing and if you want to serialize and deserialize window
917 /// handles.
918 pub fn from_id(id: u32) -> Self {
919 Self { id }
920 }
921}
922
923/// Adds a window rule.
924///
925/// Instead of using a declarative window rule system with match conditions,
926/// you supply a closure that acts on a newly opened window.
927/// You can use standard `if` statements and apply properties using the same
928/// methods that are used everywhere else in this API.
929///
930/// Note: this function is special in that if it is called, Pinnacle will wait for
931/// the provided closure to finish running before it sends windows an initial configure event.
932/// *Do not block here*. At best, short blocks will increase the time it takes for a window to
933/// open. At worst, a complete deadlock will prevent windows from opening at all.
934///
935/// # Examples
936///
937/// ```no_run
938/// # use pinnacle_api::window;
939/// # use pinnacle_api::window::DecorationMode;
940/// # use pinnacle_api::tag;
941/// window::add_window_rule(|window| {
942/// // Make Alacritty always open on the "Terminal" tag
943/// if window.app_id() == "Alacritty" {
944/// window.set_tag(&tag::get("Terminal").unwrap(), true);
945/// }
946///
947/// // Make all windows use client-side decorations
948/// window.set_decoration_mode(DecorationMode::ClientSide);
949/// });
950/// ```
951pub fn add_window_rule(mut for_all: impl FnMut(WindowHandle) + Send + 'static) {
952 let (client_outgoing, client_outgoing_to_server) = unbounded_channel();
953 let client_outgoing_to_server =
954 tokio_stream::wrappers::UnboundedReceiverStream::new(client_outgoing_to_server);
955 let mut client_incoming = Client::window()
956 .window_rule(client_outgoing_to_server)
957 .block_on_tokio()
958 .unwrap()
959 .into_inner();
960
961 let fut = async move {
962 while let Some(Ok(response)) = client_incoming.next().await {
963 let Some(response) = response.response else {
964 continue;
965 };
966
967 match response {
968 window::v1::window_rule_response::Response::NewWindow(new_window_request) => {
969 let request_id = new_window_request.request_id;
970 let window_id = new_window_request.window_id;
971
972 for_all(WindowHandle { id: window_id });
973
974 let sent = client_outgoing
975 .send(window::v1::WindowRuleRequest {
976 request: Some(window::v1::window_rule_request::Request::Finished(
977 window::v1::window_rule_request::Finished { request_id },
978 )),
979 })
980 .is_ok();
981
982 if !sent {
983 break;
984 }
985 }
986 }
987 }
988 };
989
990 tokio::spawn(fut);
991}