pinnacle_api/
output.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//! Output management.
6//!
7//! An output is the Wayland term for a monitor. It presents windows, your cursor, and other UI elements.
8//!
9//! Outputs are uniquely identified by their name, a.k.a. the name of the connector they're plugged in to.
10
11use std::str::FromStr;
12
13use futures::FutureExt;
14use pinnacle_api_defs::pinnacle::{
15    output::{
16        self,
17        v1::{
18            FocusRequest, GetEnabledRequest, GetFocusStackWindowIdsRequest, GetFocusedRequest,
19            GetInfoRequest, GetLocRequest, GetLogicalSizeRequest, GetModesRequest,
20            GetOutputsInDirRequest, GetPhysicalSizeRequest, GetPoweredRequest, GetRequest,
21            GetScaleRequest, GetTagIdsRequest, GetTransformRequest, SetLocRequest, SetModeRequest,
22            SetModelineRequest, SetPoweredRequest, SetScaleRequest, SetTransformRequest,
23            SetVrrRequest,
24        },
25    },
26    util::v1::{AbsOrRel, SetOrToggle},
27};
28
29use crate::{
30    BlockOnTokio,
31    client::Client,
32    signal::{OutputSignal, SignalHandle},
33    tag::TagHandle,
34    util::{Batch, Direction, Point, Size},
35    window::WindowHandle,
36};
37
38/// Gets handles to all currently plugged-in outputs.
39///
40/// # Examples
41///
42/// ```no_run
43/// # use pinnacle_api::output;
44/// for output in output::get_all() {
45///     println!("{} {} {}", output.make(), output.model(), output.serial());
46/// }
47/// ```
48pub fn get_all() -> impl Iterator<Item = OutputHandle> {
49    get_all_async().block_on_tokio()
50}
51
52/// Async impl for [`get_all`].
53pub async fn get_all_async() -> impl Iterator<Item = OutputHandle> {
54    Client::output()
55        .get(GetRequest {})
56        .await
57        .unwrap()
58        .into_inner()
59        .output_names
60        .into_iter()
61        .map(|name| OutputHandle { name })
62}
63
64/// Gets handles to all currently plugged-in *and enabled* outputs.
65///
66/// This ignores outputs you have explicitly disabled.
67///
68/// # Examples
69///
70/// ```no_run
71/// # use pinnacle_api::output;
72/// for output in output::get_all_enabled() {
73///     println!("{} {} {}", output.make(), output.model(), output.serial());
74/// }
75/// ```
76pub fn get_all_enabled() -> impl Iterator<Item = OutputHandle> {
77    get_all_enabled_async().block_on_tokio()
78}
79
80/// Async impl for [`get_all_enabled`].
81pub async fn get_all_enabled_async() -> impl Iterator<Item = OutputHandle> {
82    get_all_async()
83        .await
84        .batch_filter(|op| op.enabled_async().boxed(), |enabled| enabled)
85}
86
87/// Gets a handle to the output with the given name.
88///
89/// By "name", we mean the name of the connector the output is connected to.
90pub fn get_by_name(name: impl ToString) -> Option<OutputHandle> {
91    get_by_name_async(name).block_on_tokio()
92}
93
94/// Async impl for [`get_by_name`].
95pub async fn get_by_name_async(name: impl ToString) -> Option<OutputHandle> {
96    get_all_async().await.find(|op| op.name == name.to_string())
97}
98
99/// Gets a handle to the currently focused output.
100///
101/// This is currently implemented as the one that has had the most recent pointer movement.
102pub fn get_focused() -> Option<OutputHandle> {
103    get_focused_async().block_on_tokio()
104}
105
106/// Async impl for [`get_focused`].
107pub async fn get_focused_async() -> Option<OutputHandle> {
108    get_all_async()
109        .await
110        .batch_find(|op| op.focused_async().boxed(), |focused| *focused)
111}
112
113/// Runs a closure on all current and future outputs.
114///
115/// When called, this will do two things:
116/// 1. Immediately run `for_each` with all currently connected outputs.
117/// 2. Call `for_each` with any newly connected outputs.
118///
119/// Note that `for_each` will *not* run with outputs that have been unplugged and replugged.
120/// This is to prevent duplicate setup. Instead, the compositor keeps track of any tags and
121/// state the output had when unplugged and restores them on replug. This may change in the future.
122///
123/// # Examples
124///
125/// ```no_run
126/// # use pinnacle_api::output;
127/// # use pinnacle_api::tag;
128/// // Add tags 1-3 to all outputs and set tag "1" to active
129/// output::for_each_output(|op| {
130///     let mut tags = tag::add(op, ["1", "2", "3"]);
131///     tags.next().unwrap().set_active(true);
132/// });
133/// ```
134pub fn for_each_output(mut for_each: impl FnMut(&OutputHandle) + Send + 'static) {
135    for output in get_all() {
136        for_each(&output);
137    }
138
139    Client::signal_state()
140        .output_connect
141        .add_callback(Box::new(for_each));
142}
143
144/// Connects to an [`OutputSignal`].
145///
146/// # Examples
147///
148/// ```no_run
149/// # use pinnacle_api::output;
150/// # use pinnacle_api::signal::OutputSignal;
151/// output::connect_signal(OutputSignal::Connect(Box::new(|output| {
152///     println!("New output: {}", output.name());
153/// })));
154/// ```
155pub fn connect_signal(signal: OutputSignal) -> SignalHandle {
156    let mut signal_state = Client::signal_state();
157
158    match signal {
159        OutputSignal::Connect(f) => signal_state.output_connect.add_callback(f),
160        OutputSignal::Disconnect(f) => signal_state.output_disconnect.add_callback(f),
161        OutputSignal::Resize(f) => signal_state.output_resize.add_callback(f),
162        OutputSignal::Move(f) => signal_state.output_move.add_callback(f),
163        OutputSignal::PointerEnter(f) => signal_state.output_pointer_enter.add_callback(f),
164        OutputSignal::PointerLeave(f) => signal_state.output_pointer_leave.add_callback(f),
165        OutputSignal::Focused(f) => signal_state.output_focused.add_callback(f),
166    }
167}
168
169/// A handle to an output.
170///
171/// This allows you to manipulate outputs and get their properties.
172#[derive(Clone, Debug, PartialEq, Eq, Hash)]
173pub struct OutputHandle {
174    pub(crate) name: String,
175}
176
177/// The alignment to use for [`OutputHandle::set_loc_adj_to`].
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub enum Alignment {
180    /// Set above, align left borders
181    TopAlignLeft,
182    /// Set above, align centers
183    TopAlignCenter,
184    /// Set above, align right borders
185    TopAlignRight,
186    /// Set below, align left borders
187    BottomAlignLeft,
188    /// Set below, align centers
189    BottomAlignCenter,
190    /// Set below, align right borders
191    BottomAlignRight,
192    /// Set to left, align top borders
193    LeftAlignTop,
194    /// Set to left, align centers
195    LeftAlignCenter,
196    /// Set to left, align bottom borders
197    LeftAlignBottom,
198    /// Set to right, align top borders
199    RightAlignTop,
200    /// Set to right, align centers
201    RightAlignCenter,
202    /// Set to right, align bottom borders
203    RightAlignBottom,
204}
205
206/// An output transform.
207///
208/// This determines what orientation outputs will render with.
209#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
210#[repr(i32)]
211pub enum Transform {
212    /// No transform.
213    #[default]
214    Normal,
215    /// 90 degrees counter-clockwise.
216    _90,
217    /// 180 degrees counter-clockwise.
218    _180,
219    /// 270 degrees counter-clockwise.
220    _270,
221    /// Flipped vertically (across the horizontal axis).
222    Flipped,
223    /// Flipped vertically and rotated 90 degrees counter-clockwise
224    Flipped90,
225    /// Flipped vertically and rotated 180 degrees counter-clockwise
226    Flipped180,
227    /// Flipped vertically and rotated 270 degrees counter-clockwise
228    Flipped270,
229}
230
231impl TryFrom<output::v1::Transform> for Transform {
232    type Error = ();
233
234    fn try_from(value: output::v1::Transform) -> Result<Self, Self::Error> {
235        match value {
236            output::v1::Transform::Unspecified => Err(()),
237            output::v1::Transform::Normal => Ok(Transform::Normal),
238            output::v1::Transform::Transform90 => Ok(Transform::_90),
239            output::v1::Transform::Transform180 => Ok(Transform::_180),
240            output::v1::Transform::Transform270 => Ok(Transform::_270),
241            output::v1::Transform::Flipped => Ok(Transform::Flipped),
242            output::v1::Transform::Flipped90 => Ok(Transform::Flipped90),
243            output::v1::Transform::Flipped180 => Ok(Transform::Flipped180),
244            output::v1::Transform::Flipped270 => Ok(Transform::Flipped270),
245        }
246    }
247}
248
249impl From<Transform> for output::v1::Transform {
250    fn from(value: Transform) -> Self {
251        match value {
252            Transform::Normal => output::v1::Transform::Normal,
253            Transform::_90 => output::v1::Transform::Transform90,
254            Transform::_180 => output::v1::Transform::Transform180,
255            Transform::_270 => output::v1::Transform::Transform270,
256            Transform::Flipped => output::v1::Transform::Flipped,
257            Transform::Flipped90 => output::v1::Transform::Flipped90,
258            Transform::Flipped180 => output::v1::Transform::Flipped180,
259            Transform::Flipped270 => output::v1::Transform::Flipped270,
260        }
261    }
262}
263
264/// The state of variable refresh rate on an output.
265#[doc(alias = "AdaptiveSync")]
266#[doc(alias = "VariableRefreshRate")]
267#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
268pub enum Vrr {
269    /// Variable refresh rate should be off.
270    #[default]
271    Off,
272    /// Variable refresh rate should be on at all times.
273    AlwaysOn,
274    /// Variable refresh rate should be on when a window with an
275    /// active [`VrrDemand`](crate::window::VrrDemand) is visible.
276    OnDemand,
277}
278
279impl OutputHandle {
280    /// Creates an output handle from a name.
281    pub fn from_name(name: impl ToString) -> Self {
282        Self {
283            name: name.to_string(),
284        }
285    }
286
287    /// Sets the location of this output in the global space.
288    ///
289    /// On startup, Pinnacle will lay out all connected outputs starting at (0, 0)
290    /// and going to the right, with their top borders aligned.
291    ///
292    /// This method allows you to move outputs where necessary.
293    ///
294    /// Note: If you leave space between two outputs when setting their locations,
295    /// the pointer will not be able to move between them.
296    ///
297    /// # Examples
298    ///
299    /// ```no_run
300    /// # use pinnacle_api::output;
301    /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
302    /// //  - "DP-1":   ┌─────┐
303    /// //              │     │1920x1080
304    /// //              └─────┘
305    /// //  - "HDMI-1": ┌───────┐
306    /// //              │ 2560x │
307    /// //              │ 1440  │
308    /// //              └───────┘
309    /// # || {
310    /// output::get_by_name("DP-1")?.set_loc(0, 0);
311    /// output::get_by_name("HDMI-1")?.set_loc(1920, -360);
312    /// # Some(())
313    /// # };
314    /// // Results in:
315    /// //   x=0    ┌───────┐y=-360
316    /// // y=0┌─────┤       │
317    /// //    │DP-1 │HDMI-1 │
318    /// //    └─────┴───────┘
319    /// //          ^x=1920
320    /// ```
321    pub fn set_loc(&self, x: i32, y: i32) {
322        Client::output()
323            .set_loc(SetLocRequest {
324                output_name: self.name(),
325                x,
326                y,
327            })
328            .block_on_tokio()
329            .unwrap();
330    }
331
332    /// Sets this output adjacent to another one.
333    ///
334    /// This is a helper method over [`OutputHandle::set_loc`] to make laying out outputs
335    /// easier.
336    ///
337    /// `alignment` is an [`Alignment`] of how you want this output to be placed.
338    /// For example, [`TopAlignLeft`][Alignment::TopAlignLeft] will place this output
339    /// above `other` and align the left borders.
340    /// Similarly, [`RightAlignCenter`][Alignment::RightAlignCenter] will place this output
341    /// to the right of `other` and align their centers.
342    ///
343    /// # Examples
344    ///
345    /// ```no_run
346    /// # use pinnacle_api::output;
347    /// # use pinnacle_api::output::Alignment;
348    /// // Assume two monitors in order, "DP-1" and "HDMI-1", with the following dimensions:
349    /// //  - "DP-1":   ┌─────┐
350    /// //              │     │1920x1080
351    /// //              └─────┘
352    /// //  - "HDMI-1": ┌───────┐
353    /// //              │ 2560x │
354    /// //              │ 1440  │
355    /// //              └───────┘
356    /// # || {
357    /// let dp_1 = output::get_by_name("DP-1")?;
358    /// let hdmi_1 = output::get_by_name("HDMI-1")?;
359    /// dp_1.set_loc_adj_to(&hdmi_1, Alignment::BottomAlignRight);
360    /// # Some(())
361    /// # };
362    /// // Results in:
363    /// // ┌───────┐
364    /// // │       │
365    /// // │HDMI-1 │
366    /// // └──┬────┤
367    /// //    │DP-1│
368    /// //    └────┘
369    /// // Notice that "DP-1" now has the coordinates (2280, 1440) because "DP-1" is getting moved, not "HDMI-1".
370    /// // "HDMI-1" was placed at (1920, 0) during the compositor's initial output layout.
371    /// ```
372    pub fn set_loc_adj_to(&self, other: &OutputHandle, alignment: Alignment) {
373        let (self_size, other_loc, other_size) = async {
374            tokio::join!(
375                self.logical_size_async(),
376                other.loc_async(),
377                other.logical_size_async()
378            )
379        }
380        .block_on_tokio();
381
382        // poor man's try {}
383        let attempt_set_loc = || -> Option<()> {
384            let other_x = other_loc?.x;
385            let other_y = other_loc?.y;
386            let other_width = other_size?.w as i32;
387            let other_height = other_size?.h as i32;
388
389            let self_width = self_size?.w as i32;
390            let self_height = self_size?.h as i32;
391
392            use Alignment::*;
393
394            let x: i32;
395            let y: i32;
396
397            if let TopAlignLeft | TopAlignCenter | TopAlignRight | BottomAlignLeft
398            | BottomAlignCenter | BottomAlignRight = alignment
399            {
400                if let TopAlignLeft | TopAlignCenter | TopAlignRight = alignment {
401                    y = other_y - self_height;
402                } else {
403                    // bottom
404                    y = other_y + other_height;
405                }
406
407                match alignment {
408                    TopAlignLeft | BottomAlignLeft => x = other_x,
409                    TopAlignCenter | BottomAlignCenter => {
410                        x = other_x + (other_width - self_width) / 2;
411                    }
412                    TopAlignRight | BottomAlignRight => x = other_x + (other_width - self_width),
413                    _ => unreachable!(),
414                }
415            } else {
416                if let LeftAlignTop | LeftAlignCenter | LeftAlignBottom = alignment {
417                    x = other_x - self_width;
418                } else {
419                    x = other_x + other_width;
420                }
421
422                match alignment {
423                    LeftAlignTop | RightAlignTop => y = other_y,
424                    LeftAlignCenter | RightAlignCenter => {
425                        y = other_y + (other_height - self_height) / 2;
426                    }
427                    LeftAlignBottom | RightAlignBottom => {
428                        y = other_y + (other_height - self_height);
429                    }
430                    _ => unreachable!(),
431                }
432            }
433
434            self.set_loc(x, y);
435
436            Some(())
437        };
438
439        attempt_set_loc();
440    }
441
442    /// Sets this output's mode.
443    ///
444    /// If `refresh_rate_mhz` is provided, Pinnacle will attempt to use the mode with that
445    /// refresh rate. If it is not, Pinnacle will attempt to use the mode with the
446    /// highest refresh rate that matches the given size.
447    ///
448    /// The refresh rate should be given in millihertz. For example, if you want a refresh rate of
449    /// 60Hz, use 60000.
450    ///
451    /// If this output doesn't support the given mode, it will be ignored.
452    ///
453    /// # Examples
454    ///
455    /// ```no_run
456    /// # use pinnacle_api::output;
457    /// # || {
458    /// // Sets the focused output to 2560x1440 at 144Hz
459    /// output::get_focused()?.set_mode(2560, 1440, 144000);
460    /// # Some(())
461    /// # };
462    /// ```
463    pub fn set_mode(&self, width: u32, height: u32, refresh_rate_mhz: impl Into<Option<u32>>) {
464        Client::output()
465            .set_mode(SetModeRequest {
466                output_name: self.name(),
467                size: Some(pinnacle_api_defs::pinnacle::util::v1::Size { width, height }),
468                refresh_rate_mhz: refresh_rate_mhz.into(),
469                custom: false,
470            })
471            .block_on_tokio()
472            .unwrap();
473    }
474
475    /// Sets this output's mode to a custom one.
476    ///
477    /// If `refresh_rate_mhz` is provided, Pinnacle will create a new mode with that refresh rate.
478    /// If it is not, it will default to 60Hz.
479    ///
480    /// The refresh rate should be given in millihertz. For example, if you want a refresh rate of
481    /// 60Hz, use 60000.
482    ///
483    /// # Examples
484    ///
485    /// ```no_run
486    /// # use pinnacle_api::output;
487    /// # || {
488    /// // Sets the focused output to 2560x1440 at 75Hz
489    /// output::get_focused()?.set_custom_mode(2560, 1440, 75000);
490    /// # Some(())
491    /// # };
492    /// ```
493    pub fn set_custom_mode(
494        &self,
495        width: u32,
496        height: u32,
497        refresh_rate_mhz: impl Into<Option<u32>>,
498    ) {
499        Client::output()
500            .set_mode(SetModeRequest {
501                output_name: self.name(),
502                size: Some(pinnacle_api_defs::pinnacle::util::v1::Size { width, height }),
503                refresh_rate_mhz: refresh_rate_mhz.into(),
504                custom: true,
505            })
506            .block_on_tokio()
507            .unwrap();
508    }
509
510    /// Sets a custom modeline for this output.
511    ///
512    /// See `xorg.conf(5)` for more information.
513    ///
514    /// You can parse a modeline from a string of the form
515    /// "\<clock> \<hdisplay> \<hsync_start> \<hsync_end> \<htotal> \<vdisplay> \<vsync_start> \<vsync_end> \<hsync> \<vsync>".
516    ///
517    /// # Examples
518    ///
519    /// ```no_run
520    /// # use pinnacle_api::output;
521    /// # || {
522    /// let output = output::get_focused()?;
523    /// output.set_modeline("173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync".parse().unwrap());
524    /// # Some(())
525    /// # };
526    /// ```
527    pub fn set_modeline(&self, modeline: Modeline) {
528        Client::output()
529            .set_modeline(SetModelineRequest {
530                output_name: self.name(),
531                modeline: Some(modeline.into()),
532            })
533            .block_on_tokio()
534            .unwrap();
535    }
536
537    /// Sets this output's scaling factor.
538    pub fn set_scale(&self, scale: f32) {
539        Client::output()
540            .set_scale(SetScaleRequest {
541                output_name: self.name(),
542                scale,
543                abs_or_rel: AbsOrRel::Absolute.into(),
544            })
545            .block_on_tokio()
546            .unwrap();
547    }
548
549    /// Changes this output's scaling factor by a relative amount.
550    ///
551    /// # Examples
552    ///
553    /// ```no_run
554    /// # use pinnacle_api::output;
555    /// # || {
556    /// output::get_focused()?.change_scale(0.25);
557    /// output::get_focused()?.change_scale(-0.25);
558    /// # Some(())
559    /// # };
560    /// ```
561    pub fn change_scale(&self, change_by: f32) {
562        Client::output()
563            .set_scale(SetScaleRequest {
564                output_name: self.name(),
565                scale: change_by,
566                abs_or_rel: AbsOrRel::Relative.into(),
567            })
568            .block_on_tokio()
569            .unwrap();
570    }
571
572    /// Sets this output's [`Transform`].
573    ///
574    /// # Examples
575    ///
576    /// ```no_run
577    /// # use pinnacle_api::output;
578    /// # use pinnacle_api::output::Transform;
579    /// // Rotate 90 degrees counter-clockwise
580    /// # || {
581    /// output::get_focused()?.set_transform(Transform::_90);
582    /// # Some(())
583    /// # };
584    /// ```
585    pub fn set_transform(&self, transform: Transform) {
586        Client::output()
587            .set_transform(SetTransformRequest {
588                output_name: self.name(),
589                transform: output::v1::Transform::from(transform).into(),
590            })
591            .block_on_tokio()
592            .unwrap();
593    }
594
595    /// Powers on or off this output.
596    ///
597    /// This will not remove it from the space and your tags and windows
598    /// will still be interactable; only the monitor is turned off.
599    pub fn set_powered(&self, powered: bool) {
600        Client::output()
601            .set_powered(SetPoweredRequest {
602                output_name: self.name(),
603                set_or_toggle: match powered {
604                    true => SetOrToggle::Set,
605                    false => SetOrToggle::Unset,
606                }
607                .into(),
608            })
609            .block_on_tokio()
610            .unwrap();
611    }
612
613    /// Toggles the power on this output.
614    ///
615    /// This will not remove it from the space and your tags and windows
616    /// will still be interactable; only the monitor is turned off.
617    pub fn toggle_powered(&self) {
618        Client::output()
619            .set_powered(SetPoweredRequest {
620                output_name: self.name(),
621                set_or_toggle: SetOrToggle::Toggle.into(),
622            })
623            .block_on_tokio()
624            .unwrap();
625    }
626
627    /// Sets the variable refresh rate state of this output.
628    ///
629    /// See [`Vrr`] for possible states and their behaviors.
630    #[doc(alias = "set_adaptive_sync")]
631    #[doc(alias = "set_variable_refresh_rate")]
632    pub fn set_vrr(&self, vrr: Vrr) {
633        Client::output()
634            .set_vrr(SetVrrRequest {
635                output_name: self.name(),
636                vrr: match vrr {
637                    Vrr::Off => output::v1::Vrr::Off,
638                    Vrr::AlwaysOn => output::v1::Vrr::AlwaysOn,
639                    Vrr::OnDemand => output::v1::Vrr::OnDemand,
640                } as i32,
641            })
642            .block_on_tokio()
643            .unwrap();
644    }
645
646    /// Focuses this output.
647    pub fn focus(&self) {
648        Client::output()
649            .focus(FocusRequest {
650                output_name: self.name(),
651            })
652            .block_on_tokio()
653            .unwrap();
654    }
655
656    /// Gets this output's make.
657    pub fn make(&self) -> String {
658        self.make_async().block_on_tokio()
659    }
660
661    /// Async impl for [`Self::make`].
662    pub async fn make_async(&self) -> String {
663        Client::output()
664            .get_info(GetInfoRequest {
665                output_name: self.name(),
666            })
667            .await
668            .unwrap()
669            .into_inner()
670            .make
671    }
672
673    /// Gets this output's model.
674    pub fn model(&self) -> String {
675        self.model_async().block_on_tokio()
676    }
677
678    /// Async impl for [`Self::model`].
679    pub async fn model_async(&self) -> String {
680        Client::output()
681            .get_info(GetInfoRequest {
682                output_name: self.name(),
683            })
684            .await
685            .unwrap()
686            .into_inner()
687            .model
688    }
689
690    /// Gets this output's serial.
691    pub fn serial(&self) -> String {
692        self.serial_async().block_on_tokio()
693    }
694
695    /// Async impl for [`Self::serial`].
696    pub async fn serial_async(&self) -> String {
697        Client::output()
698            .get_info(GetInfoRequest {
699                output_name: self.name(),
700            })
701            .await
702            .unwrap()
703            .into_inner()
704            .serial
705    }
706
707    /// Gets this output's location in the global space.
708    ///
709    /// May return `None` if it is disabled.
710    pub fn loc(&self) -> Option<Point> {
711        self.loc_async().block_on_tokio()
712    }
713
714    /// Async impl for [`Self::loc`].
715    pub async fn loc_async(&self) -> Option<Point> {
716        Client::output()
717            .get_loc(GetLocRequest {
718                output_name: self.name(),
719            })
720            .await
721            .unwrap()
722            .into_inner()
723            .loc
724            .map(|loc| Point { x: loc.x, y: loc.y })
725    }
726
727    /// Gets this output's logical size in logical pixels.
728    ///
729    /// If this output has a scale of 1, this will equal the output's
730    /// actual pixel size. If it has a scale of 2, it will have half the
731    /// logical pixel width and height. Similarly, if it has a scale of 0.5,
732    /// it will have double the logical pixel width and height.
733    ///
734    /// May return `None` if it is disabled.
735    pub fn logical_size(&self) -> Option<Size> {
736        self.logical_size_async().block_on_tokio()
737    }
738
739    /// Async impl for [`Self::logical_size`].
740    pub async fn logical_size_async(&self) -> Option<Size> {
741        Client::output()
742            .get_logical_size(GetLogicalSizeRequest {
743                output_name: self.name(),
744            })
745            .await
746            .unwrap()
747            .into_inner()
748            .logical_size
749            .map(|size| Size {
750                w: size.width,
751                h: size.height,
752            })
753    }
754
755    /// Gets this output's current mode.
756    ///
757    /// May return `None` if it is disabled.
758    pub fn current_mode(&self) -> Option<Mode> {
759        self.current_mode_async().block_on_tokio()
760    }
761
762    /// Async impl for [`Self::current_mode`].
763    pub async fn current_mode_async(&self) -> Option<Mode> {
764        Client::output()
765            .get_modes(GetModesRequest {
766                output_name: self.name(),
767            })
768            .await
769            .unwrap()
770            .into_inner()
771            .current_mode
772            .map(|mode| Mode {
773                size: Size {
774                    w: mode.size.expect("mode should have a size").width,
775                    h: mode.size.expect("mode should have a size").height,
776                },
777                refresh_rate_mhz: mode.refresh_rate_mhz,
778            })
779    }
780
781    /// Gets this output's preferred mode.
782    ///
783    /// May return `None` if it is disabled.
784    pub fn preferred_mode(&self) -> Option<Mode> {
785        self.preferred_mode_async().block_on_tokio()
786    }
787
788    /// Async impl for [`Self::preferred_mode`].
789    pub async fn preferred_mode_async(&self) -> Option<Mode> {
790        Client::output()
791            .get_modes(GetModesRequest {
792                output_name: self.name(),
793            })
794            .await
795            .unwrap()
796            .into_inner()
797            .preferred_mode
798            .map(|mode| Mode {
799                size: Size {
800                    w: mode.size.expect("mode should have a size").width,
801                    h: mode.size.expect("mode should have a size").height,
802                },
803                refresh_rate_mhz: mode.refresh_rate_mhz,
804            })
805    }
806
807    /// Gets all modes currently known to this output.
808    pub fn modes(&self) -> impl Iterator<Item = Mode> + use<> {
809        self.modes_async().block_on_tokio()
810    }
811
812    /// Async impl for [`Self::modes`].
813    pub async fn modes_async(&self) -> impl Iterator<Item = Mode> + use<> {
814        Client::output()
815            .get_modes(GetModesRequest {
816                output_name: self.name(),
817            })
818            .await
819            .unwrap()
820            .into_inner()
821            .modes
822            .into_iter()
823            .map(|mode| Mode {
824                size: Size {
825                    w: mode.size.expect("mode should have a size").width,
826                    h: mode.size.expect("mode should have a size").height,
827                },
828                refresh_rate_mhz: mode.refresh_rate_mhz,
829            })
830    }
831
832    /// Gets this output's physical size in millimeters.
833    ///
834    /// Returns a size of (0, 0) if unknown.
835    pub fn physical_size(&self) -> Size {
836        self.physical_size_async().block_on_tokio()
837    }
838
839    /// Async impl for [`Self::physical_size`].
840    pub async fn physical_size_async(&self) -> Size {
841        Client::output()
842            .get_physical_size(GetPhysicalSizeRequest {
843                output_name: self.name(),
844            })
845            .await
846            .unwrap()
847            .into_inner()
848            .physical_size
849            .map(|size| Size {
850                w: size.width,
851                h: size.height,
852            })
853            .unwrap_or_default()
854    }
855
856    /// Gets whether or not this output is focused.
857    ///
858    /// This is currently implemented as the output with the most recent pointer motion.
859    pub fn focused(&self) -> bool {
860        self.focused_async().block_on_tokio()
861    }
862
863    /// Async impl for [`Self::focused`].
864    pub async fn focused_async(&self) -> bool {
865        Client::output()
866            .get_focused(GetFocusedRequest {
867                output_name: self.name(),
868            })
869            .await
870            .unwrap()
871            .into_inner()
872            .focused
873    }
874
875    /// Gets handles to all tags on this output.
876    pub fn tags(&self) -> impl Iterator<Item = TagHandle> + use<> {
877        self.tags_async().block_on_tokio()
878    }
879
880    /// Async impl for [`Self::tags`].
881    pub async fn tags_async(&self) -> impl Iterator<Item = TagHandle> + use<> {
882        Client::output()
883            .get_tag_ids(GetTagIdsRequest {
884                output_name: self.name(),
885            })
886            .await
887            .unwrap()
888            .into_inner()
889            .tag_ids
890            .into_iter()
891            .map(|id| TagHandle { id })
892    }
893
894    /// Gets handles to all active tags on this output.
895    pub fn active_tags(&self) -> impl Iterator<Item = TagHandle> + use<> {
896        self.active_tags_async().block_on_tokio()
897    }
898
899    /// Async impl for [`Self::active_tags`].
900    pub async fn active_tags_async(&self) -> impl Iterator<Item = TagHandle> + use<> {
901        self.tags_async()
902            .await
903            .batch_filter(|tag| tag.active_async().boxed(), |is_active| is_active)
904    }
905
906    /// Gets handles to all inactive tags on this output.
907    pub fn inactive_tags(&self) -> impl Iterator<Item = TagHandle> + use<> {
908        self.inactive_tags_async().block_on_tokio()
909    }
910
911    /// Async impl for [`Self::active_tags`].
912    pub async fn inactive_tags_async(&self) -> impl Iterator<Item = TagHandle> + use<> {
913        self.tags_async()
914            .await
915            .batch_filter(|tag| tag.active_async().boxed(), |is_active| !is_active)
916    }
917
918    /// Gets this output's current scale.
919    pub fn scale(&self) -> f32 {
920        self.scale_async().block_on_tokio()
921    }
922
923    /// Async impl for [`Self::scale`].
924    pub async fn scale_async(&self) -> f32 {
925        Client::output()
926            .get_scale(GetScaleRequest {
927                output_name: self.name(),
928            })
929            .await
930            .unwrap()
931            .into_inner()
932            .scale
933    }
934
935    /// Gets this output's current transform.
936    pub fn transform(&self) -> Transform {
937        self.transform_async().block_on_tokio()
938    }
939
940    /// Async impl for [`Self::transform`].
941    pub async fn transform_async(&self) -> Transform {
942        Client::output()
943            .get_transform(GetTransformRequest {
944                output_name: self.name(),
945            })
946            .await
947            .unwrap()
948            .into_inner()
949            .transform()
950            .try_into()
951            .unwrap_or_default()
952    }
953
954    /// Gets this window's keyboard focus stack.
955    ///
956    /// Pinnacle keeps a stack of the windows that get keyboard focus.
957    /// This can be used, for example, for an `Alt + Tab`-style keybind
958    /// that focuses the previously focused window.
959    ///
960    /// This will return the focus stack containing *all* windows on this output.
961    /// If you only want windows on active tags, see
962    /// [`OutputHandle::keyboard_focus_stack_visible`].
963    pub fn keyboard_focus_stack(&self) -> impl Iterator<Item = WindowHandle> + use<> {
964        self.keyboard_focus_stack_async().block_on_tokio()
965    }
966
967    /// Async impl for [`Self::keyboard_focus_stack`].
968    pub async fn keyboard_focus_stack_async(&self) -> impl Iterator<Item = WindowHandle> + use<> {
969        Client::output()
970            .get_focus_stack_window_ids(GetFocusStackWindowIdsRequest {
971                output_name: self.name(),
972            })
973            .await
974            .unwrap()
975            .into_inner()
976            .window_ids
977            .into_iter()
978            .map(|id| WindowHandle { id })
979    }
980
981    /// Gets this window's keyboard focus stack with only windows on active tags.
982    ///
983    /// Pinnacle keeps a stack of the windows that get keyboard focus.
984    /// This can be used, for example, for an `Alt + Tab`-style keybind
985    /// that focuses the previously focused window.
986    ///
987    /// This will return the focus stack containing only windows on active tags on this output.
988    /// If you want *all* windows on this output, see [`OutputHandle::keyboard_focus_stack`].
989    pub fn keyboard_focus_stack_visible(&self) -> impl Iterator<Item = WindowHandle> + use<> {
990        self.keyboard_focus_stack_visible_async().block_on_tokio()
991    }
992
993    /// Async impl for [`Self::keyboard_focus_stack_visible`].
994    pub async fn keyboard_focus_stack_visible_async(
995        &self,
996    ) -> impl Iterator<Item = WindowHandle> + use<> {
997        self.keyboard_focus_stack_async()
998            .await
999            .batch_filter(|win| win.is_on_active_tag_async().boxed(), |is_on| is_on)
1000    }
1001
1002    /// Gets whether this output is enabled.
1003    pub fn enabled(&self) -> bool {
1004        self.enabled_async().block_on_tokio()
1005    }
1006
1007    /// Async impl for [`Self::enabled`].
1008    pub async fn enabled_async(&self) -> bool {
1009        Client::output()
1010            .get_enabled(GetEnabledRequest {
1011                output_name: self.name(),
1012            })
1013            .await
1014            .unwrap()
1015            .into_inner()
1016            .enabled
1017    }
1018
1019    /// Gets whether or not this output is powered.
1020    ///
1021    /// Unpowered outputs are turned off but you can still interact with them.
1022    pub fn powered(&self) -> bool {
1023        self.powered_async().block_on_tokio()
1024    }
1025
1026    /// Async impl for [`Self::powered`].
1027    pub async fn powered_async(&self) -> bool {
1028        Client::output()
1029            .get_powered(GetPoweredRequest {
1030                output_name: self.name(),
1031            })
1032            .await
1033            .unwrap()
1034            .into_inner()
1035            .powered
1036    }
1037
1038    /// Gets all outputs in the provided direction, sorted closest to farthest.
1039    pub fn in_direction(&self, direction: Direction) -> impl Iterator<Item = OutputHandle> + use<> {
1040        self.in_direction_async(direction).block_on_tokio()
1041    }
1042
1043    /// Async impl for [`Self::in_direction`].
1044    pub async fn in_direction_async(
1045        &self,
1046        direction: Direction,
1047    ) -> impl Iterator<Item = OutputHandle> + use<> {
1048        let output_name = self.name();
1049
1050        let mut request = GetOutputsInDirRequest {
1051            output_name,
1052            dir: Default::default(),
1053        };
1054
1055        request.set_dir(match direction {
1056            Direction::Left => pinnacle_api_defs::pinnacle::util::v1::Dir::Left,
1057            Direction::Right => pinnacle_api_defs::pinnacle::util::v1::Dir::Right,
1058            Direction::Up => pinnacle_api_defs::pinnacle::util::v1::Dir::Up,
1059            Direction::Down => pinnacle_api_defs::pinnacle::util::v1::Dir::Down,
1060        });
1061
1062        let response = Client::output()
1063            .get_outputs_in_dir(request)
1064            .await
1065            .unwrap()
1066            .into_inner();
1067
1068        response
1069            .output_names
1070            .into_iter()
1071            .map(OutputHandle::from_name)
1072    }
1073
1074    /// Returns this output's unique name (the name of its connector).
1075    pub fn name(&self) -> String {
1076        self.name.to_string()
1077    }
1078}
1079
1080/// A possible output pixel dimension and refresh rate configuration.
1081#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
1082pub struct Mode {
1083    /// The size of the mode, in pixels.
1084    pub size: Size,
1085    /// The output's refresh rate, in millihertz.
1086    ///
1087    /// For example, 60Hz is returned as 60000.
1088    pub refresh_rate_mhz: u32,
1089}
1090
1091/// A custom modeline.
1092#[allow(missing_docs)]
1093#[derive(Copy, Clone, Debug, PartialEq, Default)]
1094pub struct Modeline {
1095    pub clock: f32,
1096    pub hdisplay: u32,
1097    pub hsync_start: u32,
1098    pub hsync_end: u32,
1099    pub htotal: u32,
1100    pub vdisplay: u32,
1101    pub vsync_start: u32,
1102    pub vsync_end: u32,
1103    pub vtotal: u32,
1104    pub hsync: bool,
1105    pub vsync: bool,
1106}
1107
1108impl From<Modeline> for output::v1::Modeline {
1109    fn from(modeline: Modeline) -> Self {
1110        output::v1::Modeline {
1111            clock: modeline.clock,
1112            hdisplay: modeline.hdisplay,
1113            hsync_start: modeline.hsync_start,
1114            hsync_end: modeline.hsync_end,
1115            htotal: modeline.htotal,
1116            vdisplay: modeline.vdisplay,
1117            vsync_start: modeline.vsync_start,
1118            vsync_end: modeline.vsync_end,
1119            vtotal: modeline.vtotal,
1120            hsync: modeline.hsync,
1121            vsync: modeline.vsync,
1122        }
1123    }
1124}
1125
1126/// Error for the `FromStr` implementation for [`Modeline`].
1127#[derive(Debug)]
1128pub struct ParseModelineError(ParseModelineErrorKind);
1129
1130#[derive(Debug)]
1131enum ParseModelineErrorKind {
1132    NoClock,
1133    InvalidClock,
1134    NoHdisplay,
1135    InvalidHdisplay,
1136    NoHsyncStart,
1137    InvalidHsyncStart,
1138    NoHsyncEnd,
1139    InvalidHsyncEnd,
1140    NoHtotal,
1141    InvalidHtotal,
1142    NoVdisplay,
1143    InvalidVdisplay,
1144    NoVsyncStart,
1145    InvalidVsyncStart,
1146    NoVsyncEnd,
1147    InvalidVsyncEnd,
1148    NoVtotal,
1149    InvalidVtotal,
1150    NoHsync,
1151    InvalidHsync,
1152    NoVsync,
1153    InvalidVsync,
1154}
1155
1156impl std::fmt::Display for ParseModelineError {
1157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1158        std::fmt::Debug::fmt(&self.0, f)
1159    }
1160}
1161
1162impl From<ParseModelineErrorKind> for ParseModelineError {
1163    fn from(value: ParseModelineErrorKind) -> Self {
1164        Self(value)
1165    }
1166}
1167
1168impl FromStr for Modeline {
1169    type Err = ParseModelineError;
1170
1171    /// Tries to convert the provided modeline string to a [`Modeline`].
1172    fn from_str(s: &str) -> Result<Self, Self::Err> {
1173        let mut args = s.split_whitespace();
1174
1175        let clock = args
1176            .next()
1177            .ok_or(ParseModelineErrorKind::NoClock)?
1178            .parse()
1179            .map_err(|_| ParseModelineErrorKind::InvalidClock)?;
1180        let hdisplay = args
1181            .next()
1182            .ok_or(ParseModelineErrorKind::NoHdisplay)?
1183            .parse()
1184            .map_err(|_| ParseModelineErrorKind::InvalidHdisplay)?;
1185        let hsync_start = args
1186            .next()
1187            .ok_or(ParseModelineErrorKind::NoHsyncStart)?
1188            .parse()
1189            .map_err(|_| ParseModelineErrorKind::InvalidHsyncStart)?;
1190        let hsync_end = args
1191            .next()
1192            .ok_or(ParseModelineErrorKind::NoHsyncEnd)?
1193            .parse()
1194            .map_err(|_| ParseModelineErrorKind::InvalidHsyncEnd)?;
1195        let htotal = args
1196            .next()
1197            .ok_or(ParseModelineErrorKind::NoHtotal)?
1198            .parse()
1199            .map_err(|_| ParseModelineErrorKind::InvalidHtotal)?;
1200        let vdisplay = args
1201            .next()
1202            .ok_or(ParseModelineErrorKind::NoVdisplay)?
1203            .parse()
1204            .map_err(|_| ParseModelineErrorKind::InvalidVdisplay)?;
1205        let vsync_start = args
1206            .next()
1207            .ok_or(ParseModelineErrorKind::NoVsyncStart)?
1208            .parse()
1209            .map_err(|_| ParseModelineErrorKind::InvalidVsyncStart)?;
1210        let vsync_end = args
1211            .next()
1212            .ok_or(ParseModelineErrorKind::NoVsyncEnd)?
1213            .parse()
1214            .map_err(|_| ParseModelineErrorKind::InvalidVsyncEnd)?;
1215        let vtotal = args
1216            .next()
1217            .ok_or(ParseModelineErrorKind::NoVtotal)?
1218            .parse()
1219            .map_err(|_| ParseModelineErrorKind::InvalidVtotal)?;
1220
1221        let hsync = match args
1222            .next()
1223            .ok_or(ParseModelineErrorKind::NoHsync)?
1224            .to_lowercase()
1225            .as_str()
1226        {
1227            "+hsync" => true,
1228            "-hsync" => false,
1229            _ => Err(ParseModelineErrorKind::InvalidHsync)?,
1230        };
1231        let vsync = match args
1232            .next()
1233            .ok_or(ParseModelineErrorKind::NoVsync)?
1234            .to_lowercase()
1235            .as_str()
1236        {
1237            "+vsync" => true,
1238            "-vsync" => false,
1239            _ => Err(ParseModelineErrorKind::InvalidVsync)?,
1240        };
1241
1242        Ok(Modeline {
1243            clock,
1244            hdisplay,
1245            hsync_start,
1246            hsync_end,
1247            htotal,
1248            vdisplay,
1249            vsync_start,
1250            vsync_end,
1251            vtotal,
1252            hsync,
1253            vsync,
1254        })
1255    }
1256}