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 futures::FutureExt;
14use pinnacle_api_defs::pinnacle::{
15    util::v1::SetOrToggle,
16    window::{
17        self,
18        v1::{
19            GetAppIdRequest, GetFocusedRequest, GetLayoutModeRequest, GetLocRequest,
20            GetSizeRequest, GetTagIdsRequest, GetTitleRequest, MoveGrabRequest, MoveToTagRequest,
21            RaiseRequest, ResizeGrabRequest, SetDecorationModeRequest, SetFloatingRequest,
22            SetFocusedRequest, SetFullscreenRequest, SetGeometryRequest, SetMaximizedRequest,
23            SetTagRequest,
24        },
25    },
26};
27use tokio::sync::mpsc::unbounded_channel;
28use tokio_stream::StreamExt;
29
30use crate::{
31    client::Client,
32    input::MouseButton,
33    signal::{SignalHandle, WindowSignal},
34    tag::TagHandle,
35    util::{Batch, Point, Size},
36    BlockOnTokio,
37};
38
39/// Gets handles to all windows.
40///
41/// # Examples
42///
43/// ```no_run
44/// # use pinnacle_api::window;
45/// for win in window::get_all() {
46///     println!("{}", win.title());
47/// }
48/// ```
49pub fn get_all() -> impl Iterator<Item = WindowHandle> {
50    get_all_async().block_on_tokio()
51}
52
53/// Async impl for [`get_all`].
54pub async fn get_all_async() -> impl Iterator<Item = WindowHandle> {
55    let window_ids = Client::window()
56        .get(pinnacle_api_defs::pinnacle::window::v1::GetRequest {})
57        .await
58        .unwrap()
59        .into_inner()
60        .window_ids;
61
62    window_ids.into_iter().map(|id| WindowHandle { id })
63}
64
65/// Gets a handle to the window with the current keyboard focus.
66///
67/// # Examples
68///
69/// ```no_run
70/// # use pinnacle_api::window;
71/// if let Some(focused) = window::get_focused() {
72///     println!("{}", focused.title());
73/// }
74/// ```
75pub fn get_focused() -> Option<WindowHandle> {
76    get_focused_async().block_on_tokio()
77}
78
79/// Async impl for [`get_focused`].
80pub async fn get_focused_async() -> Option<WindowHandle> {
81    let windows = get_all_async().await;
82
83    windows.batch_find(|win| win.focused_async().boxed(), |focused| *focused)
84}
85
86/// Begins an interactive window move.
87///
88/// This will start moving the window under the pointer until `button` is released.
89///
90/// `button` should be the mouse button that is held at the time
91/// this function is called. Otherwise, the move will not start.
92/// This is intended for use in tandem with a mousebind.
93///
94/// # Examples
95///
96/// ```no_run
97/// # use pinnacle_api::window;
98/// # use pinnacle_api::input;
99/// # use pinnacle_api::input::Mod;
100/// # use pinnacle_api::input::MouseButton;
101/// input::mousebind(Mod::SUPER, MouseButton::Left)
102///     .on_press(|| window::begin_move(MouseButton::Left));
103/// ```
104pub fn begin_move(button: MouseButton) {
105    Client::window()
106        .move_grab(MoveGrabRequest {
107            button: button.into(),
108        })
109        .block_on_tokio()
110        .unwrap();
111}
112
113/// Begins an interactive window resize.
114///
115/// This will start resizing the window under the pointer until `button` is released.
116///
117/// `button` should be the mouse button that is held at the time
118/// this function is called. Otherwise, the move will not start.
119/// This is intended for use in tandem with a mousebind.
120///
121/// # Examples
122///
123/// ```no_run
124/// # use pinnacle_api::window;
125/// # use pinnacle_api::input;
126/// # use pinnacle_api::input::Mod;
127/// # use pinnacle_api::input::MouseButton;
128/// input::mousebind(Mod::SUPER, MouseButton::Right)
129///     .on_press(|| window::begin_resize(MouseButton::Right));
130/// ```
131pub fn begin_resize(button: MouseButton) {
132    Client::window()
133        .resize_grab(ResizeGrabRequest {
134            button: button.into(),
135        })
136        .block_on_tokio()
137        .unwrap();
138}
139
140/// Connects to a [`WindowSignal`].
141///
142/// # Examples
143///
144/// ```no_run
145/// # use pinnacle_api::window;
146/// # use pinnacle_api::signal::WindowSignal;
147/// window::connect_signal(WindowSignal::PointerEnter(Box::new(|window| {
148///     window.set_focused(true);
149/// })));
150/// ```
151pub fn connect_signal(signal: WindowSignal) -> SignalHandle {
152    let mut signal_state = Client::signal_state();
153
154    match signal {
155        WindowSignal::PointerEnter(f) => signal_state.window_pointer_enter.add_callback(f),
156        WindowSignal::PointerLeave(f) => signal_state.window_pointer_leave.add_callback(f),
157        WindowSignal::Focused(f) => signal_state.window_focused.add_callback(f),
158    }
159}
160
161/// A handle to a window.
162///
163/// This allows you to manipulate the window and get its properties.
164#[derive(Debug, Clone, PartialEq, Eq, Hash)]
165pub struct WindowHandle {
166    pub(crate) id: u32,
167}
168
169/// A window's current layout mode.
170#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
171pub enum LayoutMode {
172    /// The window is tiled.
173    Tiled,
174    /// The window is floating.
175    Floating,
176    /// The window is fullscreen.
177    Fullscreen,
178    /// The window is maximized.
179    Maximized,
180}
181
182impl TryFrom<pinnacle_api_defs::pinnacle::window::v1::LayoutMode> for LayoutMode {
183    type Error = ();
184
185    fn try_from(
186        value: pinnacle_api_defs::pinnacle::window::v1::LayoutMode,
187    ) -> Result<Self, Self::Error> {
188        match value {
189            window::v1::LayoutMode::Unspecified => Err(()),
190            window::v1::LayoutMode::Tiled => Ok(LayoutMode::Tiled),
191            window::v1::LayoutMode::Floating => Ok(LayoutMode::Floating),
192            window::v1::LayoutMode::Fullscreen => Ok(LayoutMode::Fullscreen),
193            window::v1::LayoutMode::Maximized => Ok(LayoutMode::Maximized),
194        }
195    }
196}
197
198/// A mode for window decorations (titlebar, shadows, etc).
199#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
200pub enum DecorationMode {
201    /// The client should draw its own decorations.
202    ClientSide,
203    /// The server should draw decorations.
204    ServerSide,
205}
206
207impl WindowHandle {
208    /// Sends a close request to this window.
209    ///
210    /// If the window is unresponsive, it may not close.
211    pub fn close(&self) {
212        let window_id = self.id;
213        Client::window()
214            .close(pinnacle_api_defs::pinnacle::window::v1::CloseRequest { window_id })
215            .block_on_tokio()
216            .unwrap();
217    }
218
219    /// Sets this window's location and/or size.
220    ///
221    /// Only affects the floating geometry of windows. Tiled geometries are calculated
222    /// using the current layout.
223    pub fn set_geometry(
224        &self,
225        x: impl Into<Option<i32>>,
226        y: impl Into<Option<i32>>,
227        w: impl Into<Option<u32>>,
228        h: impl Into<Option<u32>>,
229    ) {
230        Client::window()
231            .set_geometry(SetGeometryRequest {
232                window_id: self.id,
233                x: x.into(),
234                y: y.into(),
235                w: w.into(),
236                h: h.into(),
237            })
238            .block_on_tokio()
239            .unwrap();
240    }
241
242    /// Sets this window to fullscreen or not.
243    pub fn set_fullscreen(&self, set: bool) {
244        let window_id = self.id;
245        Client::window()
246            .set_fullscreen(SetFullscreenRequest {
247                window_id,
248                set_or_toggle: match set {
249                    true => SetOrToggle::Set,
250                    false => SetOrToggle::Unset,
251                }
252                .into(),
253            })
254            .block_on_tokio()
255            .unwrap();
256    }
257
258    /// Toggles this window between fullscreen and not.
259    pub fn toggle_fullscreen(&self) {
260        let window_id = self.id;
261        Client::window()
262            .set_fullscreen(SetFullscreenRequest {
263                window_id,
264                set_or_toggle: SetOrToggle::Toggle.into(),
265            })
266            .block_on_tokio()
267            .unwrap();
268    }
269
270    /// Sets this window to maximized or not.
271    pub fn set_maximized(&self, set: bool) {
272        let window_id = self.id;
273        Client::window()
274            .set_maximized(SetMaximizedRequest {
275                window_id,
276                set_or_toggle: match set {
277                    true => SetOrToggle::Set,
278                    false => SetOrToggle::Unset,
279                }
280                .into(),
281            })
282            .block_on_tokio()
283            .unwrap();
284    }
285
286    /// Toggles this window between maximized and not.
287    pub fn toggle_maximized(&self) {
288        let window_id = self.id;
289        Client::window()
290            .set_maximized(SetMaximizedRequest {
291                window_id,
292                set_or_toggle: SetOrToggle::Toggle.into(),
293            })
294            .block_on_tokio()
295            .unwrap();
296    }
297
298    /// Sets this window to floating or not.
299    ///
300    /// Floating windows will not be tiled and can be moved around and resized freely.
301    pub fn set_floating(&self, set: bool) {
302        let window_id = self.id;
303        Client::window()
304            .set_floating(SetFloatingRequest {
305                window_id,
306                set_or_toggle: match set {
307                    true => SetOrToggle::Set,
308                    false => SetOrToggle::Unset,
309                }
310                .into(),
311            })
312            .block_on_tokio()
313            .unwrap();
314    }
315
316    /// Toggles this window to and from floating.
317    ///
318    /// Floating windows will not be tiled and can be moved around and resized freely.
319    pub fn toggle_floating(&self) {
320        let window_id = self.id;
321        Client::window()
322            .set_floating(SetFloatingRequest {
323                window_id,
324                set_or_toggle: SetOrToggle::Toggle.into(),
325            })
326            .block_on_tokio()
327            .unwrap();
328    }
329
330    /// Focuses or unfocuses this window.
331    pub fn set_focused(&self, set: bool) {
332        let window_id = self.id;
333        Client::window()
334            .set_focused(SetFocusedRequest {
335                window_id,
336                set_or_toggle: match set {
337                    true => SetOrToggle::Set,
338                    false => SetOrToggle::Unset,
339                }
340                .into(),
341            })
342            .block_on_tokio()
343            .unwrap();
344    }
345
346    /// Toggles this window between focused and unfocused.
347    pub fn toggle_focused(&self) {
348        let window_id = self.id;
349        Client::window()
350            .set_focused(SetFocusedRequest {
351                window_id,
352                set_or_toggle: SetOrToggle::Toggle.into(),
353            })
354            .block_on_tokio()
355            .unwrap();
356    }
357
358    /// Sets this window's decoration mode.
359    pub fn set_decoration_mode(&self, mode: DecorationMode) {
360        Client::window()
361            .set_decoration_mode(SetDecorationModeRequest {
362                window_id: self.id,
363                decoration_mode: match mode {
364                    DecorationMode::ClientSide => window::v1::DecorationMode::ClientSide,
365                    DecorationMode::ServerSide => window::v1::DecorationMode::ServerSide,
366                }
367                .into(),
368            })
369            .block_on_tokio()
370            .unwrap();
371    }
372
373    /// Moves this window to the given `tag`.
374    ///
375    /// This will remove all tags from this window then tag it with `tag`, essentially moving the
376    /// window to that tag.
377    ///
378    /// # Examples
379    ///
380    /// ```no_run
381    /// # use pinnacle_api::window;
382    /// # use pinnacle_api::tag;
383    /// # || {
384    /// // Move the focused window to tag "Code" on the focused output
385    /// window::get_focused()?.move_to_tag(&tag::get("Code")?);
386    /// # Some(())
387    /// # };
388    /// ```
389    pub fn move_to_tag(&self, tag: &TagHandle) {
390        let window_id = self.id;
391        let tag_id = tag.id;
392        Client::window()
393            .move_to_tag(MoveToTagRequest { window_id, tag_id })
394            .block_on_tokio()
395            .unwrap();
396    }
397
398    /// Sets or unsets a tag on this window.
399    ///
400    /// # Examples
401    ///
402    /// ```no_run
403    /// # use pinnacle_api::window;
404    /// # use pinnacle_api::tag;
405    /// # || {
406    /// let focused = window::get_focused()?;
407    /// let tag = tag::get("Potato")?;
408    ///
409    /// focused.set_tag(&tag, true); // `focused` now has tag "Potato"
410    /// focused.set_tag(&tag, false); // `focused` no longer has tag "Potato"
411    /// # Some(())
412    /// # };
413    /// ```
414    pub fn set_tag(&self, tag: &TagHandle, set: bool) {
415        let window_id = self.id;
416        let tag_id = tag.id;
417        Client::window()
418            .set_tag(SetTagRequest {
419                window_id,
420                tag_id,
421                set_or_toggle: match set {
422                    true => SetOrToggle::Set,
423                    false => SetOrToggle::Unset,
424                }
425                .into(),
426            })
427            .block_on_tokio()
428            .unwrap();
429    }
430
431    /// Toggles a tag on this window.
432    ///
433    /// # Examples
434    ///
435    /// ```no_run
436    /// # use pinnacle_api::window;
437    /// # use pinnacle_api::tag;
438    /// # || {
439    /// let focused = window::get_focused()?;
440    /// let tag = tag::get("Potato")?;
441    ///
442    /// focused.toggle_tag(&tag); // `focused` now has tag "Potato"
443    /// focused.toggle_tag(&tag); // `focused` no longer has tag "Potato"
444    /// # Some(())
445    /// # };
446    /// ```
447    pub fn toggle_tag(&self, tag: &TagHandle) {
448        let window_id = self.id;
449        let tag_id = tag.id;
450        Client::window()
451            .set_tag(SetTagRequest {
452                window_id,
453                tag_id,
454                set_or_toggle: SetOrToggle::Toggle.into(),
455            })
456            .block_on_tokio()
457            .unwrap();
458    }
459
460    /// Raises this window to the front.
461    pub fn raise(&self) {
462        let window_id = self.id;
463        Client::window()
464            .raise(RaiseRequest { window_id })
465            .block_on_tokio()
466            .unwrap();
467    }
468
469    /// Gets this window's current location in the global space.
470    pub fn loc(&self) -> Option<Point> {
471        self.loc_async().block_on_tokio()
472    }
473
474    /// Async impl for [`Self::loc`].
475    pub async fn loc_async(&self) -> Option<Point> {
476        let window_id = self.id;
477        Client::window()
478            .get_loc(GetLocRequest { window_id })
479            .await
480            .unwrap()
481            .into_inner()
482            .loc
483            .map(|loc| Point { x: loc.x, y: loc.y })
484    }
485
486    /// Gets this window's current size.
487    pub fn size(&self) -> Option<Size> {
488        self.size_async().block_on_tokio()
489    }
490
491    /// Async impl for [`Self::size`].
492    pub async fn size_async(&self) -> Option<Size> {
493        let window_id = self.id;
494        Client::window()
495            .get_size(GetSizeRequest { window_id })
496            .await
497            .unwrap()
498            .into_inner()
499            .size
500            .map(|size| Size {
501                w: size.width,
502                h: size.height,
503            })
504    }
505
506    /// Gets this window's app id (class if it's an xwayland window).
507    ///
508    /// If it doesn't have one, this returns an empty string.
509    pub fn app_id(&self) -> String {
510        self.app_id_async().block_on_tokio()
511    }
512
513    /// Async impl for [`Self::app_id`].
514    pub async fn app_id_async(&self) -> String {
515        let window_id = self.id;
516        Client::window()
517            .get_app_id(GetAppIdRequest { window_id })
518            .await
519            .unwrap()
520            .into_inner()
521            .app_id
522    }
523
524    /// Gets this window's title.
525    ///
526    /// If it doesn't have one, this returns an empty string.
527    pub fn title(&self) -> String {
528        self.title_async().block_on_tokio()
529    }
530
531    /// Async impl for [`Self::title`].
532    pub async fn title_async(&self) -> String {
533        let window_id = self.id;
534        Client::window()
535            .get_title(GetTitleRequest { window_id })
536            .await
537            .unwrap()
538            .into_inner()
539            .title
540    }
541
542    /// Gets whether or not this window has keyboard focus.
543    pub fn focused(&self) -> bool {
544        self.focused_async().block_on_tokio()
545    }
546
547    /// Async impl for [`Self::focused`].
548    pub async fn focused_async(&self) -> bool {
549        let window_id = self.id;
550        Client::window()
551            .get_focused(GetFocusedRequest { window_id })
552            .await
553            .unwrap()
554            .into_inner()
555            .focused
556    }
557
558    /// Gets this window's current [`LayoutMode`].
559    pub fn layout_mode(&self) -> LayoutMode {
560        self.layout_mode_async().block_on_tokio()
561    }
562
563    /// Async impl for [`Self::layout_mode`].
564    pub async fn layout_mode_async(&self) -> LayoutMode {
565        let window_id = self.id;
566        Client::window()
567            .get_layout_mode(GetLayoutModeRequest { window_id })
568            .await
569            .unwrap()
570            .into_inner()
571            .layout_mode()
572            .try_into()
573            .unwrap_or(LayoutMode::Tiled)
574    }
575
576    /// Gets whether or not this window is floating.
577    pub fn floating(&self) -> bool {
578        self.floating_async().block_on_tokio()
579    }
580
581    /// Async impl for [`Self::floating`].
582    pub async fn floating_async(&self) -> bool {
583        self.layout_mode_async().await == LayoutMode::Floating
584    }
585
586    /// Gets whether or not this window is fullscreen.
587    pub fn fullscreen(&self) -> bool {
588        self.fullscreen_async().block_on_tokio()
589    }
590
591    /// Async impl for [`Self::fullscreen`].
592    pub async fn fullscreen_async(&self) -> bool {
593        self.layout_mode_async().await == LayoutMode::Fullscreen
594    }
595
596    /// Gets whether or not this window is maximized.
597    pub fn maximized(&self) -> bool {
598        self.maximized_async().block_on_tokio()
599    }
600
601    /// Async impl for [`Self::maximized`].
602    pub async fn maximized_async(&self) -> bool {
603        self.layout_mode_async().await == LayoutMode::Maximized
604    }
605
606    /// Gets handles to all tags on this window.
607    pub fn tags(&self) -> impl Iterator<Item = TagHandle> {
608        self.tags_async().block_on_tokio()
609    }
610
611    /// Async impl for [`Self::tags`].
612    pub async fn tags_async(&self) -> impl Iterator<Item = TagHandle> {
613        let window_id = self.id;
614        Client::window()
615            .get_tag_ids(GetTagIdsRequest { window_id })
616            .await
617            .unwrap()
618            .into_inner()
619            .tag_ids
620            .into_iter()
621            .map(|id| TagHandle { id })
622    }
623
624    /// Gets whether or not this window has an active tag.
625    pub fn is_on_active_tag(&self) -> bool {
626        self.is_on_active_tag_async().block_on_tokio()
627    }
628
629    /// Async impl for [`Self::is_on_active_tag`].
630    pub async fn is_on_active_tag_async(&self) -> bool {
631        self.tags_async()
632            .await
633            .batch_find(|tag| tag.active_async().boxed(), |active| *active)
634            .is_some()
635    }
636
637    /// Gets this window's raw compositor id.
638    pub fn id(&self) -> u32 {
639        self.id
640    }
641
642    /// Creates a window handle from an ID.
643    ///
644    /// Note: This is mostly for testing and if you want to serialize and deserialize window
645    /// handles.
646    pub fn from_id(id: u32) -> Self {
647        Self { id }
648    }
649}
650
651/// Adds a window rule.
652///
653/// Instead of using a declarative window rule system with match conditions,
654/// you supply a closure that acts on a newly opened window.
655/// You can use standard `if` statements and apply properties using the same
656/// methods that are used everywhere else in this API.
657///
658/// Note: this function is special in that if it is called, Pinnacle will wait for
659/// the provided closure to finish running before it sends windows an initial configure event.
660/// *Do not block here*. At best, short blocks will increase the time it takes for a window to
661/// open. At worst, a complete deadlock will prevent windows from opening at all.
662///
663/// # Examples
664///
665/// ```no_run
666/// # use pinnacle_api::window;
667/// # use pinnacle_api::window::DecorationMode;
668/// # use pinnacle_api::tag;
669/// window::add_window_rule(|window| {
670///     // Make Alacritty always open on the "Terminal" tag
671///     if window.app_id() == "Alacritty" {
672///         window.set_tag(&tag::get("Terminal").unwrap(), true);
673///     }
674///
675///     // Make all windows use client-side decorations
676///     window.set_decoration_mode(DecorationMode::ClientSide);
677/// });
678/// ```
679pub fn add_window_rule(mut for_all: impl FnMut(WindowHandle) + Send + 'static) {
680    let (client_outgoing, client_outgoing_to_server) = unbounded_channel();
681    let client_outgoing_to_server =
682        tokio_stream::wrappers::UnboundedReceiverStream::new(client_outgoing_to_server);
683    let mut client_incoming = Client::window()
684        .window_rule(client_outgoing_to_server)
685        .block_on_tokio()
686        .unwrap()
687        .into_inner();
688
689    let fut = async move {
690        while let Some(Ok(response)) = client_incoming.next().await {
691            let Some(response) = response.response else {
692                continue;
693            };
694
695            match response {
696                window::v1::window_rule_response::Response::NewWindow(new_window_request) => {
697                    let request_id = new_window_request.request_id;
698                    let window_id = new_window_request.window_id;
699
700                    for_all(WindowHandle { id: window_id });
701
702                    let sent = client_outgoing
703                        .send(window::v1::WindowRuleRequest {
704                            request: Some(window::v1::window_rule_request::Request::Finished(
705                                window::v1::window_rule_request::Finished { request_id },
706                            )),
707                        })
708                        .is_ok();
709
710                    if !sent {
711                        break;
712                    }
713                }
714            }
715        }
716    };
717
718    tokio::spawn(fut);
719}