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}