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