niri_ipc/
lib.rs

1//! Types for communicating with niri via IPC.
2//!
3//! After connecting to the niri socket, you can send [`Request`]s. Niri will process them one by
4//! one, in order, and to each request it will respond with a single [`Reply`], which is a `Result`
5//! wrapping a [`Response`].
6//!
7//! If you send a [`Request::EventStream`], niri will *stop* reading subsequent [`Request`]s, and
8//! will start continuously writing compositor [`Event`]s to the socket. If you'd like to read an
9//! event stream and write more requests at the same time, you need to use two IPC sockets.
10//!
11//! <div class="warning">
12//!
13//! Requests are *always* processed separately. Time passes between requests, even when sending
14//! multiple requests to the socket at once. For example, sending [`Request::Workspaces`] and
15//! [`Request::Windows`] together may not return consistent results (e.g. a window may open on a
16//! new workspace in-between the two responses). This goes for actions too: sending
17//! [`Action::FocusWindow`] and <code>[Action::CloseWindow] { id: None }</code> together may close
18//! the wrong window because a different window got focused in-between these requests.
19//!
20//! </div>
21//!
22//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
23//! it is a fairly simple helper, so if you need async, or if you're using a different language,
24//! you are encouraged to communicate with the socket manually.
25//!
26//! 1. Read the socket filesystem path from [`socket::SOCKET_PATH_ENV`] (`$NIRI_SOCKET`).
27//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
28//!    up with a line break and a flush, or just flush and shutdown the write end of the socket.
29//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
30//! 4. You can keep writing [`Request`]s, each on a single line, and read [`Reply`]s, also each on a
31//!    separate line.
32//! 5. After you request an event stream, niri will keep responding with JSON-formatted [`Event`]s,
33//!    on a single line each.
34//!
35//! ## Backwards compatibility
36//!
37//! This crate follows the niri version. It is **not** API-stable in terms of the Rust semver. In
38//! particular, expect new struct fields and enum variants to be added in patch version bumps.
39//!
40//! Use an exact version requirement to avoid breaking changes:
41//!
42//! ```toml
43//! [dependencies]
44//! niri-ipc = "=25.11.0"
45//! ```
46//!
47//! ## Features
48//!
49//! This crate defines the following features:
50//! - `json-schema`: derives the [schemars](https://lib.rs/crates/schemars) `JsonSchema` trait for
51//!   the types.
52//! - `clap`: derives the clap CLI parsing traits for some types. Used internally by niri itself.
53#![warn(missing_docs)]
54
55use std::collections::HashMap;
56use std::str::FromStr;
57use std::time::Duration;
58
59use serde::{Deserialize, Serialize};
60
61pub mod socket;
62pub mod state;
63
64/// Request from client to niri.
65#[derive(Debug, Serialize, Deserialize, Clone)]
66#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
67pub enum Request {
68    /// Request the version string for the running niri instance.
69    Version,
70    /// Request information about connected outputs.
71    Outputs,
72    /// Request information about workspaces.
73    Workspaces,
74    /// Request information about open windows.
75    Windows,
76    /// Request information about layer-shell surfaces.
77    Layers,
78    /// Request information about the configured keyboard layouts.
79    KeyboardLayouts,
80    /// Request information about the focused output.
81    FocusedOutput,
82    /// Request information about the focused window.
83    FocusedWindow,
84    /// Request picking a window and get its information.
85    PickWindow,
86    /// Request picking a color from the screen.
87    PickColor,
88    /// Perform an action.
89    Action(Action),
90    /// Change output configuration temporarily.
91    ///
92    /// The configuration is changed temporarily and not saved into the config file. If the output
93    /// configuration subsequently changes in the config file, these temporary changes will be
94    /// forgotten.
95    Output {
96        /// Output name.
97        output: String,
98        /// Configuration to apply.
99        action: OutputAction,
100    },
101    /// Start continuously receiving events from the compositor.
102    ///
103    /// The compositor should reply with `Reply::Ok(Response::Handled)`, then continuously send
104    /// [`Event`]s, one per line.
105    ///
106    /// The event stream will always give you the full current state up-front. For example, the
107    /// first workspace-related event you will receive will be [`Event::WorkspacesChanged`]
108    /// containing the full current workspaces state. You *do not* need to separately send
109    /// [`Request::Workspaces`] when using the event stream.
110    ///
111    /// Where reasonable, event stream state updates are atomic, though this is not always the
112    /// case. For example, a window may end up with a workspace id for a workspace that had already
113    /// been removed. This can happen if the corresponding [`Event::WorkspacesChanged`] arrives
114    /// before the corresponding [`Event::WindowOpenedOrChanged`].
115    EventStream,
116    /// Respond with an error (for testing error handling).
117    ReturnError,
118    /// Request information about the overview.
119    OverviewState,
120    /// Request information about screencasts.
121    Casts,
122}
123
124/// Reply from niri to client.
125///
126/// Every request gets one reply.
127///
128/// * If an error had occurred, it will be an `Reply::Err`.
129/// * If the request does not need any particular response, it will be
130///   `Reply::Ok(Response::Handled)`. Kind of like an `Ok(())`.
131/// * Otherwise, it will be `Reply::Ok(response)` with one of the other [`Response`] variants.
132pub type Reply = Result<Response, String>;
133
134/// Successful response from niri to client.
135#[derive(Debug, Serialize, Deserialize, Clone)]
136#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
137pub enum Response {
138    /// A request that does not need a response was handled successfully.
139    Handled,
140    /// The version string for the running niri instance.
141    Version(String),
142    /// Information about connected outputs.
143    ///
144    /// Map from output name to output info.
145    Outputs(HashMap<String, Output>),
146    /// Information about workspaces.
147    Workspaces(Vec<Workspace>),
148    /// Information about open windows.
149    Windows(Vec<Window>),
150    /// Information about layer-shell surfaces.
151    Layers(Vec<LayerSurface>),
152    /// Information about the keyboard layout.
153    KeyboardLayouts(KeyboardLayouts),
154    /// Information about the focused output.
155    FocusedOutput(Option<Output>),
156    /// Information about the focused window.
157    FocusedWindow(Option<Window>),
158    /// Information about the picked window.
159    PickedWindow(Option<Window>),
160    /// Information about the picked color.
161    PickedColor(Option<PickedColor>),
162    /// Output configuration change result.
163    OutputConfigChanged(OutputConfigChanged),
164    /// Information about the overview.
165    OverviewState(Overview),
166    /// Information about screencasts.
167    Casts(Vec<Cast>),
168}
169
170/// Overview information.
171#[derive(Serialize, Deserialize, Debug, Clone)]
172#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
173pub struct Overview {
174    /// Whether the overview is currently open.
175    pub is_open: bool,
176}
177
178/// Color picked from the screen.
179#[derive(Serialize, Deserialize, Debug, Clone)]
180#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
181pub struct PickedColor {
182    /// Color values as red, green, blue, each ranging from 0.0 to 1.0.
183    pub rgb: [f64; 3],
184}
185
186/// Actions that niri can perform.
187// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all,
188// variants from niri-config should be present here.
189#[derive(Serialize, Deserialize, Debug, Clone)]
190#[cfg_attr(feature = "clap", derive(clap::Parser))]
191#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
192#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
193#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
194pub enum Action {
195    /// Exit niri.
196    Quit {
197        /// Skip the "Press Enter to confirm" prompt.
198        #[cfg_attr(feature = "clap", arg(short, long))]
199        skip_confirmation: bool,
200    },
201    /// Power off all monitors via DPMS.
202    PowerOffMonitors {},
203    /// Power on all monitors via DPMS.
204    PowerOnMonitors {},
205    /// Spawn a command.
206    Spawn {
207        /// Command to spawn.
208        #[cfg_attr(feature = "clap", arg(last = true, required = true))]
209        command: Vec<String>,
210    },
211    /// Spawn a command through the shell.
212    SpawnSh {
213        /// Command to run.
214        #[cfg_attr(feature = "clap", arg(last = true, required = true))]
215        command: String,
216    },
217    /// Do a screen transition.
218    DoScreenTransition {
219        /// Delay in milliseconds for the screen to freeze before starting the transition.
220        #[cfg_attr(feature = "clap", arg(short, long))]
221        delay_ms: Option<u16>,
222    },
223    /// Open the screenshot UI.
224    Screenshot {
225        ///  Whether to show the mouse pointer by default in the screenshot UI.
226        #[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
227        show_pointer: bool,
228
229        /// Path to save the screenshot to.
230        ///
231        /// The path must be absolute, otherwise an error is returned.
232        ///
233        /// If `None`, the screenshot is saved according to the `screenshot-path` config setting.
234        #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))]
235        path: Option<String>,
236    },
237    /// Screenshot the focused screen.
238    ScreenshotScreen {
239        /// Write the screenshot to disk in addition to putting it in your clipboard.
240        ///
241        /// The screenshot is saved according to the `screenshot-path` config setting.
242        #[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
243        write_to_disk: bool,
244
245        /// Whether to include the mouse pointer in the screenshot.
246        #[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
247        show_pointer: bool,
248
249        /// Path to save the screenshot to.
250        ///
251        /// The path must be absolute, otherwise an error is returned.
252        ///
253        /// If `None`, the screenshot is saved according to the `screenshot-path` config setting.
254        #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))]
255        path: Option<String>,
256    },
257    /// Screenshot a window.
258    #[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
259    ScreenshotWindow {
260        /// Id of the window to screenshot.
261        ///
262        /// If `None`, uses the focused window.
263        #[cfg_attr(feature = "clap", arg(long))]
264        id: Option<u64>,
265        /// Write the screenshot to disk in addition to putting it in your clipboard.
266        ///
267        /// The screenshot is saved according to the `screenshot-path` config setting.
268        #[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
269        write_to_disk: bool,
270
271        /// Whether to include the mouse pointer in the screenshot.
272        ///
273        /// The pointer will be included only if the window is currently receiving pointer input
274        /// (usually this means the pointer is on top of the window).
275        #[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = false))]
276        show_pointer: bool,
277
278        /// Path to save the screenshot to.
279        ///
280        /// The path must be absolute, otherwise an error is returned.
281        ///
282        /// If `None`, the screenshot is saved according to the `screenshot-path` config setting.
283        #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))]
284        path: Option<String>,
285    },
286    /// Enable or disable the keyboard shortcuts inhibitor (if any) for the focused surface.
287    ToggleKeyboardShortcutsInhibit {},
288    /// Close a window.
289    #[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
290    CloseWindow {
291        /// Id of the window to close.
292        ///
293        /// If `None`, uses the focused window.
294        #[cfg_attr(feature = "clap", arg(long))]
295        id: Option<u64>,
296    },
297    /// Toggle fullscreen on a window.
298    #[cfg_attr(
299        feature = "clap",
300        clap(about = "Toggle fullscreen on the focused window")
301    )]
302    FullscreenWindow {
303        /// Id of the window to toggle fullscreen of.
304        ///
305        /// If `None`, uses the focused window.
306        #[cfg_attr(feature = "clap", arg(long))]
307        id: Option<u64>,
308    },
309    /// Toggle windowed (fake) fullscreen on a window.
310    #[cfg_attr(
311        feature = "clap",
312        clap(about = "Toggle windowed (fake) fullscreen on the focused window")
313    )]
314    ToggleWindowedFullscreen {
315        /// Id of the window to toggle windowed fullscreen of.
316        ///
317        /// If `None`, uses the focused window.
318        #[cfg_attr(feature = "clap", arg(long))]
319        id: Option<u64>,
320    },
321    /// Focus a window by id.
322    FocusWindow {
323        /// Id of the window to focus.
324        #[cfg_attr(feature = "clap", arg(long))]
325        id: u64,
326    },
327    /// Focus a window in the focused column by index.
328    FocusWindowInColumn {
329        /// Index of the window in the column.
330        ///
331        /// The index starts from 1 for the topmost window.
332        #[cfg_attr(feature = "clap", arg())]
333        index: u8,
334    },
335    /// Focus the previously focused window.
336    FocusWindowPrevious {},
337    /// Focus the column to the left.
338    FocusColumnLeft {},
339    /// Focus the column to the right.
340    FocusColumnRight {},
341    /// Focus the first column.
342    FocusColumnFirst {},
343    /// Focus the last column.
344    FocusColumnLast {},
345    /// Focus the next column to the right, looping if at end.
346    FocusColumnRightOrFirst {},
347    /// Focus the next column to the left, looping if at start.
348    FocusColumnLeftOrLast {},
349    /// Focus a column by index.
350    FocusColumn {
351        /// Index of the column to focus.
352        ///
353        /// The index starts from 1 for the first column.
354        #[cfg_attr(feature = "clap", arg())]
355        index: usize,
356    },
357    /// Focus the window or the monitor above.
358    FocusWindowOrMonitorUp {},
359    /// Focus the window or the monitor below.
360    FocusWindowOrMonitorDown {},
361    /// Focus the column or the monitor to the left.
362    FocusColumnOrMonitorLeft {},
363    /// Focus the column or the monitor to the right.
364    FocusColumnOrMonitorRight {},
365    /// Focus the window below.
366    FocusWindowDown {},
367    /// Focus the window above.
368    FocusWindowUp {},
369    /// Focus the window below or the column to the left.
370    FocusWindowDownOrColumnLeft {},
371    /// Focus the window below or the column to the right.
372    FocusWindowDownOrColumnRight {},
373    /// Focus the window above or the column to the left.
374    FocusWindowUpOrColumnLeft {},
375    /// Focus the window above or the column to the right.
376    FocusWindowUpOrColumnRight {},
377    /// Focus the window or the workspace below.
378    FocusWindowOrWorkspaceDown {},
379    /// Focus the window or the workspace above.
380    FocusWindowOrWorkspaceUp {},
381    /// Focus the topmost window.
382    FocusWindowTop {},
383    /// Focus the bottommost window.
384    FocusWindowBottom {},
385    /// Focus the window below or the topmost window.
386    FocusWindowDownOrTop {},
387    /// Focus the window above or the bottommost window.
388    FocusWindowUpOrBottom {},
389    /// Move the focused column to the left.
390    MoveColumnLeft {},
391    /// Move the focused column to the right.
392    MoveColumnRight {},
393    /// Move the focused column to the start of the workspace.
394    MoveColumnToFirst {},
395    /// Move the focused column to the end of the workspace.
396    MoveColumnToLast {},
397    /// Move the focused column to the left or to the monitor to the left.
398    MoveColumnLeftOrToMonitorLeft {},
399    /// Move the focused column to the right or to the monitor to the right.
400    MoveColumnRightOrToMonitorRight {},
401    /// Move the focused column to a specific index on its workspace.
402    MoveColumnToIndex {
403        /// New index for the column.
404        ///
405        /// The index starts from 1 for the first column.
406        #[cfg_attr(feature = "clap", arg())]
407        index: usize,
408    },
409    /// Move the focused window down in a column.
410    MoveWindowDown {},
411    /// Move the focused window up in a column.
412    MoveWindowUp {},
413    /// Move the focused window down in a column or to the workspace below.
414    MoveWindowDownOrToWorkspaceDown {},
415    /// Move the focused window up in a column or to the workspace above.
416    MoveWindowUpOrToWorkspaceUp {},
417    /// Consume or expel a window left.
418    #[cfg_attr(
419        feature = "clap",
420        clap(about = "Consume or expel the focused window left")
421    )]
422    ConsumeOrExpelWindowLeft {
423        /// Id of the window to consume or expel.
424        ///
425        /// If `None`, uses the focused window.
426        #[cfg_attr(feature = "clap", arg(long))]
427        id: Option<u64>,
428    },
429    /// Consume or expel a window right.
430    #[cfg_attr(
431        feature = "clap",
432        clap(about = "Consume or expel the focused window right")
433    )]
434    ConsumeOrExpelWindowRight {
435        /// Id of the window to consume or expel.
436        ///
437        /// If `None`, uses the focused window.
438        #[cfg_attr(feature = "clap", arg(long))]
439        id: Option<u64>,
440    },
441    /// Consume the window to the right into the focused column.
442    ConsumeWindowIntoColumn {},
443    /// Expel the bottom window from the focused column.
444    ExpelWindowFromColumn {},
445    /// Swap focused window with one to the right.
446    SwapWindowRight {},
447    /// Swap focused window with one to the left.
448    SwapWindowLeft {},
449    /// Toggle the focused column between normal and tabbed display.
450    ToggleColumnTabbedDisplay {},
451    /// Set the display mode of the focused column.
452    SetColumnDisplay {
453        /// Display mode to set.
454        #[cfg_attr(feature = "clap", arg())]
455        display: ColumnDisplay,
456    },
457    /// Center the focused column on the screen.
458    CenterColumn {},
459    /// Center a window on the screen.
460    #[cfg_attr(
461        feature = "clap",
462        clap(about = "Center the focused window on the screen")
463    )]
464    CenterWindow {
465        /// Id of the window to center.
466        ///
467        /// If `None`, uses the focused window.
468        #[cfg_attr(feature = "clap", arg(long))]
469        id: Option<u64>,
470    },
471    /// Center all fully visible columns on the screen.
472    CenterVisibleColumns {},
473    /// Focus the workspace below.
474    FocusWorkspaceDown {},
475    /// Focus the workspace above.
476    FocusWorkspaceUp {},
477    /// Focus a workspace by reference (index or name).
478    FocusWorkspace {
479        /// Reference (index or name) of the workspace to focus.
480        #[cfg_attr(feature = "clap", arg())]
481        reference: WorkspaceReferenceArg,
482    },
483    /// Focus the previous workspace.
484    FocusWorkspacePrevious {},
485    /// Move the focused window to the workspace below.
486    MoveWindowToWorkspaceDown {
487        /// Whether the focus should follow the target workspace.
488        ///
489        /// If `true` (the default), the focus will follow the window to the new workspace. If
490        /// `false`, the focus will remain on the original workspace.
491        #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
492        focus: bool,
493    },
494    /// Move the focused window to the workspace above.
495    MoveWindowToWorkspaceUp {
496        /// Whether the focus should follow the target workspace.
497        ///
498        /// If `true` (the default), the focus will follow the window to the new workspace. If
499        /// `false`, the focus will remain on the original workspace.
500        #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
501        focus: bool,
502    },
503    /// Move a window to a workspace.
504    #[cfg_attr(
505        feature = "clap",
506        clap(about = "Move the focused window to a workspace by reference (index or name)")
507    )]
508    MoveWindowToWorkspace {
509        /// Id of the window to move.
510        ///
511        /// If `None`, uses the focused window.
512        #[cfg_attr(feature = "clap", arg(long))]
513        window_id: Option<u64>,
514
515        /// Reference (index or name) of the workspace to move the window to.
516        #[cfg_attr(feature = "clap", arg())]
517        reference: WorkspaceReferenceArg,
518
519        /// Whether the focus should follow the moved window.
520        ///
521        /// If `true` (the default) and the window to move is focused, the focus will follow the
522        /// window to the new workspace. If `false`, the focus will remain on the original
523        /// workspace.
524        #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
525        focus: bool,
526    },
527    /// Move the focused column to the workspace below.
528    MoveColumnToWorkspaceDown {
529        /// Whether the focus should follow the target workspace.
530        ///
531        /// If `true` (the default), the focus will follow the column to the new workspace. If
532        /// `false`, the focus will remain on the original workspace.
533        #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
534        focus: bool,
535    },
536    /// Move the focused column to the workspace above.
537    MoveColumnToWorkspaceUp {
538        /// Whether the focus should follow the target workspace.
539        ///
540        /// If `true` (the default), the focus will follow the column to the new workspace. If
541        /// `false`, the focus will remain on the original workspace.
542        #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
543        focus: bool,
544    },
545    /// Move the focused column to a workspace by reference (index or name).
546    MoveColumnToWorkspace {
547        /// Reference (index or name) of the workspace to move the column to.
548        #[cfg_attr(feature = "clap", arg())]
549        reference: WorkspaceReferenceArg,
550
551        /// Whether the focus should follow the target workspace.
552        ///
553        /// If `true` (the default), the focus will follow the column to the new workspace. If
554        /// `false`, the focus will remain on the original workspace.
555        #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
556        focus: bool,
557    },
558    /// Move the focused workspace down.
559    MoveWorkspaceDown {},
560    /// Move the focused workspace up.
561    MoveWorkspaceUp {},
562    /// Move a workspace to a specific index on its monitor.
563    #[cfg_attr(
564        feature = "clap",
565        clap(about = "Move the focused workspace to a specific index on its monitor")
566    )]
567    MoveWorkspaceToIndex {
568        /// New index for the workspace.
569        #[cfg_attr(feature = "clap", arg())]
570        index: usize,
571
572        /// Reference (index or name) of the workspace to move.
573        ///
574        /// If `None`, uses the focused workspace.
575        #[cfg_attr(feature = "clap", arg(long))]
576        reference: Option<WorkspaceReferenceArg>,
577    },
578    /// Set the name of a workspace.
579    #[cfg_attr(
580        feature = "clap",
581        clap(about = "Set the name of the focused workspace")
582    )]
583    SetWorkspaceName {
584        /// New name for the workspace.
585        #[cfg_attr(feature = "clap", arg())]
586        name: String,
587
588        /// Reference (index or name) of the workspace to name.
589        ///
590        /// If `None`, uses the focused workspace.
591        #[cfg_attr(feature = "clap", arg(long))]
592        workspace: Option<WorkspaceReferenceArg>,
593    },
594    /// Unset the name of a workspace.
595    #[cfg_attr(
596        feature = "clap",
597        clap(about = "Unset the name of the focused workspace")
598    )]
599    UnsetWorkspaceName {
600        /// Reference (index or name) of the workspace to unname.
601        ///
602        /// If `None`, uses the focused workspace.
603        #[cfg_attr(feature = "clap", arg())]
604        reference: Option<WorkspaceReferenceArg>,
605    },
606    /// Focus the monitor to the left.
607    FocusMonitorLeft {},
608    /// Focus the monitor to the right.
609    FocusMonitorRight {},
610    /// Focus the monitor below.
611    FocusMonitorDown {},
612    /// Focus the monitor above.
613    FocusMonitorUp {},
614    /// Focus the previous monitor.
615    FocusMonitorPrevious {},
616    /// Focus the next monitor.
617    FocusMonitorNext {},
618    /// Focus a monitor by name.
619    FocusMonitor {
620        /// Name of the output to focus.
621        #[cfg_attr(feature = "clap", arg())]
622        output: String,
623    },
624    /// Move the focused window to the monitor to the left.
625    MoveWindowToMonitorLeft {},
626    /// Move the focused window to the monitor to the right.
627    MoveWindowToMonitorRight {},
628    /// Move the focused window to the monitor below.
629    MoveWindowToMonitorDown {},
630    /// Move the focused window to the monitor above.
631    MoveWindowToMonitorUp {},
632    /// Move the focused window to the previous monitor.
633    MoveWindowToMonitorPrevious {},
634    /// Move the focused window to the next monitor.
635    MoveWindowToMonitorNext {},
636    /// Move a window to a specific monitor.
637    #[cfg_attr(
638        feature = "clap",
639        clap(about = "Move the focused window to a specific monitor")
640    )]
641    MoveWindowToMonitor {
642        /// Id of the window to move.
643        ///
644        /// If `None`, uses the focused window.
645        #[cfg_attr(feature = "clap", arg(long))]
646        id: Option<u64>,
647
648        /// The target output name.
649        #[cfg_attr(feature = "clap", arg())]
650        output: String,
651    },
652    /// Move the focused column to the monitor to the left.
653    MoveColumnToMonitorLeft {},
654    /// Move the focused column to the monitor to the right.
655    MoveColumnToMonitorRight {},
656    /// Move the focused column to the monitor below.
657    MoveColumnToMonitorDown {},
658    /// Move the focused column to the monitor above.
659    MoveColumnToMonitorUp {},
660    /// Move the focused column to the previous monitor.
661    MoveColumnToMonitorPrevious {},
662    /// Move the focused column to the next monitor.
663    MoveColumnToMonitorNext {},
664    /// Move the focused column to a specific monitor.
665    MoveColumnToMonitor {
666        /// The target output name.
667        #[cfg_attr(feature = "clap", arg())]
668        output: String,
669    },
670    /// Change the width of a window.
671    #[cfg_attr(
672        feature = "clap",
673        clap(about = "Change the width of the focused window")
674    )]
675    SetWindowWidth {
676        /// Id of the window whose width to set.
677        ///
678        /// If `None`, uses the focused window.
679        #[cfg_attr(feature = "clap", arg(long))]
680        id: Option<u64>,
681
682        /// How to change the width.
683        #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
684        change: SizeChange,
685    },
686    /// Change the height of a window.
687    #[cfg_attr(
688        feature = "clap",
689        clap(about = "Change the height of the focused window")
690    )]
691    SetWindowHeight {
692        /// Id of the window whose height to set.
693        ///
694        /// If `None`, uses the focused window.
695        #[cfg_attr(feature = "clap", arg(long))]
696        id: Option<u64>,
697
698        /// How to change the height.
699        #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
700        change: SizeChange,
701    },
702    /// Reset the height of a window back to automatic.
703    #[cfg_attr(
704        feature = "clap",
705        clap(about = "Reset the height of the focused window back to automatic")
706    )]
707    ResetWindowHeight {
708        /// Id of the window whose height to reset.
709        ///
710        /// If `None`, uses the focused window.
711        #[cfg_attr(feature = "clap", arg(long))]
712        id: Option<u64>,
713    },
714    /// Switch between preset column widths.
715    SwitchPresetColumnWidth {},
716    /// Switch between preset column widths backwards.
717    SwitchPresetColumnWidthBack {},
718    /// Switch between preset window widths.
719    SwitchPresetWindowWidth {
720        /// Id of the window whose width to switch.
721        ///
722        /// If `None`, uses the focused window.
723        #[cfg_attr(feature = "clap", arg(long))]
724        id: Option<u64>,
725    },
726    /// Switch between preset window widths backwards.
727    SwitchPresetWindowWidthBack {
728        /// Id of the window whose width to switch.
729        ///
730        /// If `None`, uses the focused window.
731        #[cfg_attr(feature = "clap", arg(long))]
732        id: Option<u64>,
733    },
734    /// Switch between preset window heights.
735    SwitchPresetWindowHeight {
736        /// Id of the window whose height to switch.
737        ///
738        /// If `None`, uses the focused window.
739        #[cfg_attr(feature = "clap", arg(long))]
740        id: Option<u64>,
741    },
742    /// Switch between preset window heights backwards.
743    SwitchPresetWindowHeightBack {
744        /// Id of the window whose height to switch.
745        ///
746        /// If `None`, uses the focused window.
747        #[cfg_attr(feature = "clap", arg(long))]
748        id: Option<u64>,
749    },
750    /// Toggle the maximized state of the focused column.
751    MaximizeColumn {},
752    /// Toggle the maximized-to-edges state of the focused window.
753    MaximizeWindowToEdges {
754        /// Id of the window to maximize.
755        ///
756        /// If `None`, uses the focused window.
757        #[cfg_attr(feature = "clap", arg(long))]
758        id: Option<u64>,
759    },
760    /// Change the width of the focused column.
761    SetColumnWidth {
762        /// How to change the width.
763        #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
764        change: SizeChange,
765    },
766    /// Expand the focused column to space not taken up by other fully visible columns.
767    ExpandColumnToAvailableWidth {},
768    /// Switch between keyboard layouts.
769    SwitchLayout {
770        /// Layout to switch to.
771        #[cfg_attr(feature = "clap", arg())]
772        layout: LayoutSwitchTarget,
773    },
774    /// Show the hotkey overlay.
775    ShowHotkeyOverlay {},
776    /// Move the focused workspace to the monitor to the left.
777    MoveWorkspaceToMonitorLeft {},
778    /// Move the focused workspace to the monitor to the right.
779    MoveWorkspaceToMonitorRight {},
780    /// Move the focused workspace to the monitor below.
781    MoveWorkspaceToMonitorDown {},
782    /// Move the focused workspace to the monitor above.
783    MoveWorkspaceToMonitorUp {},
784    /// Move the focused workspace to the previous monitor.
785    MoveWorkspaceToMonitorPrevious {},
786    /// Move the focused workspace to the next monitor.
787    MoveWorkspaceToMonitorNext {},
788    /// Move a workspace to a specific monitor.
789    #[cfg_attr(
790        feature = "clap",
791        clap(about = "Move the focused workspace to a specific monitor")
792    )]
793    MoveWorkspaceToMonitor {
794        /// The target output name.
795        #[cfg_attr(feature = "clap", arg())]
796        output: String,
797
798        // Reference (index or name) of the workspace to move.
799        ///
800        /// If `None`, uses the focused workspace.
801        #[cfg_attr(feature = "clap", arg(long))]
802        reference: Option<WorkspaceReferenceArg>,
803    },
804    /// Toggle a debug tint on windows.
805    ToggleDebugTint {},
806    /// Toggle visualization of render element opaque regions.
807    DebugToggleOpaqueRegions {},
808    /// Toggle visualization of output damage.
809    DebugToggleDamage {},
810    /// Move the focused window between the floating and the tiling layout.
811    ToggleWindowFloating {
812        /// Id of the window to move.
813        ///
814        /// If `None`, uses the focused window.
815        #[cfg_attr(feature = "clap", arg(long))]
816        id: Option<u64>,
817    },
818    /// Move the focused window to the floating layout.
819    MoveWindowToFloating {
820        /// Id of the window to move.
821        ///
822        /// If `None`, uses the focused window.
823        #[cfg_attr(feature = "clap", arg(long))]
824        id: Option<u64>,
825    },
826    /// Move the focused window to the tiling layout.
827    MoveWindowToTiling {
828        /// Id of the window to move.
829        ///
830        /// If `None`, uses the focused window.
831        #[cfg_attr(feature = "clap", arg(long))]
832        id: Option<u64>,
833    },
834    /// Switches focus to the floating layout.
835    FocusFloating {},
836    /// Switches focus to the tiling layout.
837    FocusTiling {},
838    /// Toggles the focus between the floating and the tiling layout.
839    SwitchFocusBetweenFloatingAndTiling {},
840    /// Move a floating window on screen.
841    #[cfg_attr(feature = "clap", clap(about = "Move the floating window on screen"))]
842    MoveFloatingWindow {
843        /// Id of the window to move.
844        ///
845        /// If `None`, uses the focused window.
846        #[cfg_attr(feature = "clap", arg(long))]
847        id: Option<u64>,
848
849        /// How to change the X position.
850        #[cfg_attr(
851            feature = "clap",
852            arg(short, long, default_value = "+0", allow_hyphen_values = true)
853        )]
854        x: PositionChange,
855
856        /// How to change the Y position.
857        #[cfg_attr(
858            feature = "clap",
859            arg(short, long, default_value = "+0", allow_hyphen_values = true)
860        )]
861        y: PositionChange,
862    },
863    /// Toggle the opacity of a window.
864    #[cfg_attr(
865        feature = "clap",
866        clap(about = "Toggle the opacity of the focused window")
867    )]
868    ToggleWindowRuleOpacity {
869        /// Id of the window.
870        ///
871        /// If `None`, uses the focused window.
872        #[cfg_attr(feature = "clap", arg(long))]
873        id: Option<u64>,
874    },
875    /// Set the dynamic cast target to a window.
876    #[cfg_attr(
877        feature = "clap",
878        clap(about = "Set the dynamic cast target to the focused window")
879    )]
880    SetDynamicCastWindow {
881        /// Id of the window to target.
882        ///
883        /// If `None`, uses the focused window.
884        #[cfg_attr(feature = "clap", arg(long))]
885        id: Option<u64>,
886    },
887    /// Set the dynamic cast target to a monitor.
888    #[cfg_attr(
889        feature = "clap",
890        clap(about = "Set the dynamic cast target to the focused monitor")
891    )]
892    SetDynamicCastMonitor {
893        /// Name of the output to target.
894        ///
895        /// If `None`, uses the focused output.
896        #[cfg_attr(feature = "clap", arg())]
897        output: Option<String>,
898    },
899    /// Clear the dynamic cast target, making it show nothing.
900    ClearDynamicCastTarget {},
901    /// Stop a PipeWire screencast.
902    ///
903    /// wlr-screencopy screencasts cannot currently be stopped via IPC.
904    StopCast {
905        /// Session ID of the screencast to stop.
906        ///
907        /// If the session has multiple screencast streams, this will stop all of them.
908        #[cfg_attr(feature = "clap", arg(long))]
909        session_id: u64,
910    },
911    /// Toggle (open/close) the Overview.
912    ToggleOverview {},
913    /// Open the Overview.
914    OpenOverview {},
915    /// Close the Overview.
916    CloseOverview {},
917    /// Toggle urgent status of a window.
918    ToggleWindowUrgent {
919        /// Id of the window to toggle urgent.
920        #[cfg_attr(feature = "clap", arg(long))]
921        id: u64,
922    },
923    /// Set urgent status of a window.
924    SetWindowUrgent {
925        /// Id of the window to set urgent.
926        #[cfg_attr(feature = "clap", arg(long))]
927        id: u64,
928    },
929    /// Unset urgent status of a window.
930    UnsetWindowUrgent {
931        /// Id of the window to unset urgent.
932        #[cfg_attr(feature = "clap", arg(long))]
933        id: u64,
934    },
935    /// Reload the config file.
936    ///
937    /// Can be useful for scripts changing the config file, to avoid waiting the small duration for
938    /// niri's config file watcher to notice the changes.
939    LoadConfigFile {},
940}
941
942/// Change in window or column size.
943#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
944#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
945pub enum SizeChange {
946    /// Set the size in logical pixels.
947    SetFixed(i32),
948    /// Set the size as a proportion of the working area.
949    SetProportion(f64),
950    /// Add or subtract to the current size in logical pixels.
951    AdjustFixed(i32),
952    /// Add or subtract to the current size as a proportion of the working area.
953    AdjustProportion(f64),
954}
955
956/// Change in floating window position.
957#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
958#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
959pub enum PositionChange {
960    /// Set the position in logical pixels.
961    SetFixed(f64),
962    /// Set the position as a proportion of the working area.
963    SetProportion(f64),
964    /// Add or subtract to the current position in logical pixels.
965    AdjustFixed(f64),
966    /// Add or subtract to the current position as a proportion of the working area.
967    AdjustProportion(f64),
968}
969
970/// Workspace reference (id, index or name) to operate on.
971#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
972#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
973pub enum WorkspaceReferenceArg {
974    /// Id of the workspace.
975    Id(u64),
976    /// Index of the workspace.
977    Index(u8),
978    /// Name of the workspace.
979    Name(String),
980}
981
982/// Layout to switch to.
983#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
984#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
985pub enum LayoutSwitchTarget {
986    /// The next configured layout.
987    Next,
988    /// The previous configured layout.
989    Prev,
990    /// The specific layout by index.
991    Index(u8),
992}
993
994/// How windows display in a column.
995#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
996#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
997pub enum ColumnDisplay {
998    /// Windows are tiled vertically across the working area height.
999    Normal,
1000    /// Windows are in tabs.
1001    Tabbed,
1002}
1003
1004/// Output actions that niri can perform.
1005// Variants in this enum should match the spelling of the ones in niri-config. Most thigs from
1006// niri-config should be present here.
1007#[derive(Serialize, Deserialize, Debug, Clone)]
1008#[cfg_attr(feature = "clap", derive(clap::Parser))]
1009#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
1010#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
1011#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1012pub enum OutputAction {
1013    /// Turn off the output.
1014    Off,
1015    /// Turn on the output.
1016    On,
1017    /// Set the output mode.
1018    Mode {
1019        /// Mode to set, or "auto" for automatic selection.
1020        ///
1021        /// Run `niri msg outputs` to see the available modes.
1022        #[cfg_attr(feature = "clap", arg())]
1023        mode: ModeToSet,
1024    },
1025    /// Set a custom output mode.
1026    CustomMode {
1027        /// Custom mode to set.
1028        #[cfg_attr(feature = "clap", arg())]
1029        mode: ConfiguredMode,
1030    },
1031    /// Set a custom VESA CVT modeline.
1032    #[cfg_attr(feature = "clap", arg())]
1033    Modeline {
1034        /// The rate at which pixels are drawn in MHz.
1035        #[cfg_attr(feature = "clap", arg())]
1036        clock: f64,
1037        /// Horizontal active pixels.
1038        #[cfg_attr(feature = "clap", arg())]
1039        hdisplay: u16,
1040        /// Horizontal sync pulse start position in pixels.
1041        #[cfg_attr(feature = "clap", arg())]
1042        hsync_start: u16,
1043        /// Horizontal sync pulse end position in pixels.
1044        #[cfg_attr(feature = "clap", arg())]
1045        hsync_end: u16,
1046        /// Total horizontal number of pixels before resetting the horizontal drawing position to
1047        /// zero.
1048        #[cfg_attr(feature = "clap", arg())]
1049        htotal: u16,
1050
1051        /// Vertical active pixels.
1052        #[cfg_attr(feature = "clap", arg())]
1053        vdisplay: u16,
1054        /// Vertical sync pulse start position in pixels.
1055        #[cfg_attr(feature = "clap", arg())]
1056        vsync_start: u16,
1057        /// Vertical sync pulse end position in pixels.
1058        #[cfg_attr(feature = "clap", arg())]
1059        vsync_end: u16,
1060        /// Total vertical number of pixels before resetting the vertical drawing position to zero.
1061        #[cfg_attr(feature = "clap", arg())]
1062        vtotal: u16,
1063        /// Horizontal sync polarity: "+hsync" or "-hsync".
1064        #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
1065        hsync_polarity: HSyncPolarity,
1066        /// Vertical sync polarity: "+vsync" or "-vsync".
1067        #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
1068        vsync_polarity: VSyncPolarity,
1069    },
1070    /// Set the output scale.
1071    Scale {
1072        /// Scale factor to set, or "auto" for automatic selection.
1073        #[cfg_attr(feature = "clap", arg())]
1074        scale: ScaleToSet,
1075    },
1076    /// Set the output transform.
1077    Transform {
1078        /// Transform to set, counter-clockwise.
1079        #[cfg_attr(feature = "clap", arg())]
1080        transform: Transform,
1081    },
1082    /// Set the output position.
1083    Position {
1084        /// Position to set, or "auto" for automatic selection.
1085        #[cfg_attr(feature = "clap", command(subcommand))]
1086        position: PositionToSet,
1087    },
1088    /// Set the variable refresh rate mode.
1089    Vrr {
1090        /// Variable refresh rate mode to set.
1091        #[cfg_attr(feature = "clap", command(flatten))]
1092        vrr: VrrToSet,
1093    },
1094}
1095
1096/// Output mode to set.
1097#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1098#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1099pub enum ModeToSet {
1100    /// Niri will pick the mode automatically.
1101    Automatic,
1102    /// Specific mode.
1103    Specific(ConfiguredMode),
1104}
1105
1106/// Output mode as set in the config file.
1107#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1108#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1109pub struct ConfiguredMode {
1110    /// Width in physical pixels.
1111    pub width: u16,
1112    /// Height in physical pixels.
1113    pub height: u16,
1114    /// Refresh rate.
1115    pub refresh: Option<f64>,
1116}
1117
1118/// Modeline horizontal syncing polarity.
1119#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1120#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1121pub enum HSyncPolarity {
1122    /// Positive polarity.
1123    PHSync,
1124    /// Negative polarity.
1125    NHSync,
1126}
1127
1128/// Modeline vertical syncing polarity.
1129#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1130#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1131pub enum VSyncPolarity {
1132    /// Positive polarity.
1133    PVSync,
1134    /// Negative polarity.
1135    NVSync,
1136}
1137
1138/// Output scale to set.
1139#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1140#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1141pub enum ScaleToSet {
1142    /// Niri will pick the scale automatically.
1143    Automatic,
1144    /// Specific scale.
1145    Specific(f64),
1146}
1147
1148/// Output position to set.
1149#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1150#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
1151#[cfg_attr(feature = "clap", command(subcommand_value_name = "POSITION"))]
1152#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Position Values"))]
1153#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1154pub enum PositionToSet {
1155    /// Position the output automatically.
1156    #[cfg_attr(feature = "clap", command(name = "auto"))]
1157    Automatic,
1158    /// Set a specific position.
1159    #[cfg_attr(feature = "clap", command(name = "set"))]
1160    Specific(ConfiguredPosition),
1161}
1162
1163/// Output position as set in the config file.
1164#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1165#[cfg_attr(feature = "clap", derive(clap::Args))]
1166#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1167pub struct ConfiguredPosition {
1168    /// Logical X position.
1169    pub x: i32,
1170    /// Logical Y position.
1171    pub y: i32,
1172}
1173
1174/// Output VRR to set.
1175#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1176#[cfg_attr(feature = "clap", derive(clap::Args))]
1177#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1178pub struct VrrToSet {
1179    /// Whether to enable variable refresh rate.
1180    #[cfg_attr(
1181        feature = "clap",
1182        arg(
1183            value_name = "ON|OFF",
1184            action = clap::ArgAction::Set,
1185            value_parser = clap::builder::BoolishValueParser::new(),
1186            hide_possible_values = true,
1187        ),
1188    )]
1189    pub vrr: bool,
1190    /// Only enable when the output shows a window matching the variable-refresh-rate window rule.
1191    #[cfg_attr(feature = "clap", arg(long))]
1192    pub on_demand: bool,
1193}
1194
1195/// Connected output.
1196#[derive(Debug, Serialize, Deserialize, Clone)]
1197#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1198pub struct Output {
1199    /// Name of the output.
1200    pub name: String,
1201    /// Textual description of the manufacturer.
1202    pub make: String,
1203    /// Textual description of the model.
1204    pub model: String,
1205    /// Serial of the output, if known.
1206    pub serial: Option<String>,
1207    /// Physical width and height of the output in millimeters, if known.
1208    pub physical_size: Option<(u32, u32)>,
1209    /// Available modes for the output.
1210    pub modes: Vec<Mode>,
1211    /// Index of the current mode in [`Self::modes`].
1212    ///
1213    /// `None` if the output is disabled.
1214    pub current_mode: Option<usize>,
1215    /// Whether the current_mode is a custom mode.
1216    pub is_custom_mode: bool,
1217    /// Whether the output supports variable refresh rate.
1218    pub vrr_supported: bool,
1219    /// Whether variable refresh rate is enabled on the output.
1220    pub vrr_enabled: bool,
1221    /// Logical output information.
1222    ///
1223    /// `None` if the output is not mapped to any logical output (for example, if it is disabled).
1224    pub logical: Option<LogicalOutput>,
1225}
1226
1227/// Output mode.
1228#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
1229#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1230pub struct Mode {
1231    /// Width in physical pixels.
1232    pub width: u16,
1233    /// Height in physical pixels.
1234    pub height: u16,
1235    /// Refresh rate in millihertz.
1236    pub refresh_rate: u32,
1237    /// Whether this mode is preferred by the monitor.
1238    pub is_preferred: bool,
1239}
1240
1241/// Logical output in the compositor's coordinate space.
1242#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
1243#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1244pub struct LogicalOutput {
1245    /// Logical X position.
1246    pub x: i32,
1247    /// Logical Y position.
1248    pub y: i32,
1249    /// Width in logical pixels.
1250    pub width: u32,
1251    /// Height in logical pixels.
1252    pub height: u32,
1253    /// Scale factor.
1254    pub scale: f64,
1255    /// Transform.
1256    pub transform: Transform,
1257}
1258
1259/// Output transform, which goes counter-clockwise.
1260#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1261#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
1262#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1263pub enum Transform {
1264    /// Untransformed.
1265    Normal,
1266    /// Rotated by 90°.
1267    #[serde(rename = "90")]
1268    _90,
1269    /// Rotated by 180°.
1270    #[serde(rename = "180")]
1271    _180,
1272    /// Rotated by 270°.
1273    #[serde(rename = "270")]
1274    _270,
1275    /// Flipped horizontally.
1276    Flipped,
1277    /// Rotated by 90° and flipped horizontally.
1278    #[cfg_attr(feature = "clap", value(name("flipped-90")))]
1279    Flipped90,
1280    /// Flipped vertically.
1281    #[cfg_attr(feature = "clap", value(name("flipped-180")))]
1282    Flipped180,
1283    /// Rotated by 270° and flipped horizontally.
1284    #[cfg_attr(feature = "clap", value(name("flipped-270")))]
1285    Flipped270,
1286}
1287
1288/// Toplevel window.
1289#[derive(Serialize, Deserialize, Debug, Clone)]
1290#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1291pub struct Window {
1292    /// Unique id of this window.
1293    ///
1294    /// This id remains constant while this window is open.
1295    ///
1296    /// Do not assume that window ids will always increase without wrapping, or start at 1. That is
1297    /// an implementation detail subject to change. For example, ids may change to be randomly
1298    /// generated for each new window.
1299    pub id: u64,
1300    /// Title, if set.
1301    pub title: Option<String>,
1302    /// Application ID, if set.
1303    pub app_id: Option<String>,
1304    /// Process ID that created the Wayland connection for this window, if known.
1305    ///
1306    /// Currently, windows created by xdg-desktop-portal-gnome will have a `None` PID, but this may
1307    /// change in the future.
1308    pub pid: Option<i32>,
1309    /// Id of the workspace this window is on, if any.
1310    pub workspace_id: Option<u64>,
1311    /// Whether this window is currently focused.
1312    ///
1313    /// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
1314    pub is_focused: bool,
1315    /// Whether this window is currently floating.
1316    ///
1317    /// If the window isn't floating then it is in the tiling layout.
1318    pub is_floating: bool,
1319    /// Whether this window requests your attention.
1320    pub is_urgent: bool,
1321    /// Position- and size-related properties of the window.
1322    pub layout: WindowLayout,
1323    /// Timestamp when the window was most recently focused.
1324    ///
1325    /// This timestamp is intended for most-recently-used window switchers, i.e. Alt-Tab. It only
1326    /// updates after some debounce time so that quick window switching doesn't mark intermediate
1327    /// windows as recently focused.
1328    ///
1329    /// The timestamp comes from the monotonic clock.
1330    pub focus_timestamp: Option<Timestamp>,
1331}
1332
1333/// A moment in time.
1334#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1335#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1336pub struct Timestamp {
1337    /// Number of whole seconds.
1338    pub secs: u64,
1339    /// Fractional part of the timestamp in nanoseconds (10<sup>-9</sup> seconds).
1340    pub nanos: u32,
1341}
1342
1343/// Position- and size-related properties of a [`Window`].
1344///
1345/// Optional properties will be unset for some windows, do not rely on them being present. Whether
1346/// some optional properties are present or absent for certain window types may change across niri
1347/// releases.
1348///
1349/// All sizes and positions are in *logical pixels* unless stated otherwise. Logical sizes may be
1350/// fractional. For example, at 1.25 monitor scale, a 2-physical-pixel-wide window border is 1.6
1351/// logical pixels wide.
1352///
1353/// This struct contains positions and sizes both for full tiles ([`Self::tile_size`],
1354/// [`Self::tile_pos_in_workspace_view`]) and the window geometry ([`Self::window_size`],
1355/// [`Self::window_offset_in_tile`]). For visual displays, use the tile properties, as they
1356/// correspond to what the user visually considers "window". The window properties on the other
1357/// hand are mainly useful when you need to know the underlying Wayland window sizes, e.g. for
1358/// application debugging.
1359#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1360#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1361pub struct WindowLayout {
1362    /// Location of a tiled window within a workspace: (column index, tile index in column).
1363    ///
1364    /// The indices are 1-based, i.e. the leftmost column is at index 1 and the topmost tile in a
1365    /// column is at index 1. This is consistent with [`Action::FocusColumn`] and
1366    /// [`Action::FocusWindowInColumn`].
1367    pub pos_in_scrolling_layout: Option<(usize, usize)>,
1368    /// Size of the tile this window is in, including decorations like borders.
1369    pub tile_size: (f64, f64),
1370    /// Size of the window's visual geometry itself.
1371    ///
1372    /// Does not include niri decorations like borders.
1373    ///
1374    /// Currently, Wayland toplevel windows can only be integer-sized in logical pixels, even
1375    /// though it doesn't necessarily align to physical pixels.
1376    pub window_size: (i32, i32),
1377    /// Tile position within the current view of the workspace.
1378    ///
1379    /// This is the same "workspace view" as in gradients' `relative-to` in the niri config.
1380    pub tile_pos_in_workspace_view: Option<(f64, f64)>,
1381    /// Location of the window's visual geometry within its tile.
1382    ///
1383    /// This includes things like border sizes. For fullscreened fixed-size windows this includes
1384    /// the distance from the corner of the black backdrop to the corner of the (centered) window
1385    /// contents.
1386    pub window_offset_in_tile: (f64, f64),
1387}
1388
1389/// Output configuration change result.
1390#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1391#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1392pub enum OutputConfigChanged {
1393    /// The target output was connected and the change was applied.
1394    Applied,
1395    /// The target output was not found, the change will be applied when it is connected.
1396    OutputWasMissing,
1397}
1398
1399/// A workspace.
1400#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1401#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1402pub struct Workspace {
1403    /// Unique id of this workspace.
1404    ///
1405    /// This id remains constant regardless of the workspace moving around and across monitors.
1406    ///
1407    /// Do not assume that workspace ids will always increase without wrapping, or start at 1. That
1408    /// is an implementation detail subject to change. For example, ids may change to be randomly
1409    /// generated for each new workspace.
1410    pub id: u64,
1411    /// Index of the workspace on its monitor.
1412    ///
1413    /// This is the same index you can use for requests like `niri msg action focus-workspace`.
1414    ///
1415    /// This index *will change* as you move and re-order workspace. It is merely the workspace's
1416    /// current position on its monitor. Workspaces on different monitors can have the same index.
1417    ///
1418    /// If you need a unique workspace id that doesn't change, see [`Self::id`].
1419    pub idx: u8,
1420    /// Optional name of the workspace.
1421    pub name: Option<String>,
1422    /// Name of the output that the workspace is on.
1423    ///
1424    /// Can be `None` if no outputs are currently connected.
1425    pub output: Option<String>,
1426    /// Whether the workspace currently has an urgent window in its output.
1427    pub is_urgent: bool,
1428    /// Whether the workspace is currently active on its output.
1429    ///
1430    /// Every output has one active workspace, the one that is currently visible on that output.
1431    pub is_active: bool,
1432    /// Whether the workspace is currently focused.
1433    ///
1434    /// There's only one focused workspace across all outputs.
1435    pub is_focused: bool,
1436    /// Id of the active window on this workspace, if any.
1437    pub active_window_id: Option<u64>,
1438}
1439
1440/// Configured keyboard layouts.
1441#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1442#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1443pub struct KeyboardLayouts {
1444    /// XKB names of the configured layouts.
1445    pub names: Vec<String>,
1446    /// Index of the currently active layout in `names`.
1447    pub current_idx: u8,
1448}
1449
1450/// A layer-shell layer.
1451#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1452#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1453pub enum Layer {
1454    /// The background layer.
1455    Background,
1456    /// The bottom layer.
1457    Bottom,
1458    /// The top layer.
1459    Top,
1460    /// The overlay layer.
1461    Overlay,
1462}
1463
1464/// Keyboard interactivity modes for a layer-shell surface.
1465#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1466#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1467pub enum LayerSurfaceKeyboardInteractivity {
1468    /// Surface cannot receive keyboard focus.
1469    None,
1470    /// Surface receives keyboard focus whenever possible.
1471    Exclusive,
1472    /// Surface receives keyboard focus on demand, e.g. when clicked.
1473    OnDemand,
1474}
1475
1476/// A layer-shell surface.
1477#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1478#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1479pub struct LayerSurface {
1480    /// Namespace provided by the layer-shell client.
1481    pub namespace: String,
1482    /// Name of the output the surface is on.
1483    pub output: String,
1484    /// Layer that the surface is on.
1485    pub layer: Layer,
1486    /// The surface's keyboard interactivity mode.
1487    pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
1488}
1489
1490/// A screencast.
1491#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1492#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1493pub struct Cast {
1494    /// Stream ID of the screencast that uniquely identifies it.
1495    pub stream_id: u64,
1496    /// Session ID of the screencast.
1497    ///
1498    /// A session can have multiple screencast streams. Then multiple `Cast`s will have the same
1499    /// `session_id`. Though, usually there's only one stream per session.
1500    ///
1501    /// Do not confuse `session_id` with [`stream_id`](Self::stream_id).
1502    pub session_id: u64,
1503    /// Kind of this screencast.
1504    pub kind: CastKind,
1505    /// Target being captured.
1506    pub target: CastTarget,
1507    /// Whether this is a Dynamic Cast Target screencast.
1508    ///
1509    /// Meaning that actions like `SetDynamicCastWindow` will act on this screencast.
1510    ///
1511    /// Keep in mind that the target can change even if this is `false`.
1512    pub is_dynamic_target: bool,
1513    /// Whether the cast is currently streaming frames.
1514    ///
1515    /// This can be `false` for example when switching away to a different scene in OBS, which
1516    /// pauses the stream.
1517    pub is_active: bool,
1518    /// Process ID of the screencast consumer, if known.
1519    ///
1520    /// Currently, only wlr-screencopy screencasts can have a pid.
1521    pub pid: Option<i32>,
1522    /// PipeWire node ID of the screencast stream.
1523    ///
1524    /// This is `None` for wlr-screencopy casts, and also for PipeWire casts before the node is
1525    /// created (when the cast is just starting up).
1526    pub pw_node_id: Option<u32>,
1527}
1528
1529/// Kind of screencast.
1530#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1531#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1532pub enum CastKind {
1533    /// PipeWire screencast, typically via xdg-desktop-portal-gnome.
1534    PipeWire,
1535    /// wlr-screencopy protocol screencast.
1536    ///
1537    /// Tools like wf-recorder, and the xdg-desktop-portal-wlr portal.
1538    ///
1539    /// Only wlr-screencopy with damage tracking is reported here. Screencopy without damage is
1540    /// treated as a regular screenshot and not reported as a screencast.
1541    WlrScreencopy,
1542}
1543
1544/// Target of a screencast.
1545#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1546#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1547pub enum CastTarget {
1548    /// The target is not yet set, or was cleared.
1549    Nothing {},
1550    /// Casting an output.
1551    Output {
1552        /// Name of the screencasted output.
1553        name: String,
1554    },
1555    /// Casting a window.
1556    Window {
1557        /// ID of the screencasted window.
1558        id: u64,
1559    },
1560}
1561
1562/// A compositor event.
1563#[derive(Serialize, Deserialize, Debug, Clone)]
1564#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1565pub enum Event {
1566    /// The workspace configuration has changed.
1567    WorkspacesChanged {
1568        /// The new workspace configuration.
1569        ///
1570        /// This configuration completely replaces the previous configuration. I.e. if any
1571        /// workspaces are missing from here, then they were deleted.
1572        workspaces: Vec<Workspace>,
1573    },
1574    /// The workspace urgency changed.
1575    WorkspaceUrgencyChanged {
1576        /// Id of the workspace.
1577        id: u64,
1578        /// Whether this workspace has an urgent window.
1579        urgent: bool,
1580    },
1581    /// A workspace was activated on an output.
1582    ///
1583    /// This doesn't always mean the workspace became focused, just that it's now the active
1584    /// workspace on its output. All other workspaces on the same output become inactive.
1585    WorkspaceActivated {
1586        /// Id of the newly active workspace.
1587        id: u64,
1588        /// Whether this workspace also became focused.
1589        ///
1590        /// If `true`, this is now the single focused workspace. All other workspaces are no longer
1591        /// focused, but they may remain active on their respective outputs.
1592        focused: bool,
1593    },
1594    /// An active window changed on a workspace.
1595    WorkspaceActiveWindowChanged {
1596        /// Id of the workspace on which the active window changed.
1597        workspace_id: u64,
1598        /// Id of the new active window, if any.
1599        active_window_id: Option<u64>,
1600    },
1601    /// The window configuration has changed.
1602    WindowsChanged {
1603        /// The new window configuration.
1604        ///
1605        /// This configuration completely replaces the previous configuration. I.e. if any windows
1606        /// are missing from here, then they were closed.
1607        windows: Vec<Window>,
1608    },
1609    /// A new toplevel window was opened, or an existing toplevel window changed.
1610    WindowOpenedOrChanged {
1611        /// The new or updated window.
1612        ///
1613        /// If the window is focused, all other windows are no longer focused.
1614        window: Window,
1615    },
1616    /// A toplevel window was closed.
1617    WindowClosed {
1618        /// Id of the removed window.
1619        id: u64,
1620    },
1621    /// Window focus changed.
1622    ///
1623    /// All other windows are no longer focused.
1624    WindowFocusChanged {
1625        /// Id of the newly focused window, or `None` if no window is now focused.
1626        id: Option<u64>,
1627    },
1628    /// Window focus timestamp changed.
1629    ///
1630    /// This event is separate from [`Event::WindowFocusChanged`] because the focus timestamp only
1631    /// updates after some debounce time so that quick window switching doesn't mark intermediate
1632    /// windows as recently focused.
1633    WindowFocusTimestampChanged {
1634        /// Id of the window.
1635        id: u64,
1636        /// The new focus timestamp.
1637        focus_timestamp: Option<Timestamp>,
1638    },
1639    /// Window urgency changed.
1640    WindowUrgencyChanged {
1641        /// Id of the window.
1642        id: u64,
1643        /// The new urgency state of the window.
1644        urgent: bool,
1645    },
1646    /// The layout of one or more windows has changed.
1647    WindowLayoutsChanged {
1648        /// Pairs consisting of a window id and new layout information for the window.
1649        changes: Vec<(u64, WindowLayout)>,
1650    },
1651    /// The configured keyboard layouts have changed.
1652    KeyboardLayoutsChanged {
1653        /// The new keyboard layout configuration.
1654        keyboard_layouts: KeyboardLayouts,
1655    },
1656    /// The keyboard layout switched.
1657    KeyboardLayoutSwitched {
1658        /// Index of the newly active layout.
1659        idx: u8,
1660    },
1661    /// The overview was opened or closed.
1662    OverviewOpenedOrClosed {
1663        /// The new state of the overview.
1664        is_open: bool,
1665    },
1666    /// The configuration was reloaded.
1667    ///
1668    /// You will always receive this event when connecting to the event stream, indicating the last
1669    /// config load attempt.
1670    ConfigLoaded {
1671        /// Whether the loading failed.
1672        ///
1673        /// For example, the config file couldn't be parsed.
1674        failed: bool,
1675    },
1676    /// A screenshot was captured.
1677    ScreenshotCaptured {
1678        /// The file path where the screenshot was saved, if it was written to disk.
1679        ///
1680        /// If `None`, the screenshot was either only copied to the clipboard, or the path couldn't
1681        /// be converted to a `String` (e.g. contained invalid UTF-8 bytes).
1682        path: Option<String>,
1683    },
1684    /// The screencasts have changed.
1685    CastsChanged {
1686        /// The new screencast information.
1687        ///
1688        /// This configuration completely replaces the previous configuration. I.e. if any casts
1689        /// are missing from here, then they were stopped.
1690        casts: Vec<Cast>,
1691    },
1692    /// A screencast started, or an existing cast changed.
1693    CastStartedOrChanged {
1694        /// The cast that started or changed.
1695        cast: Cast,
1696    },
1697    /// A screencast stopped.
1698    CastStopped {
1699        /// Stream ID of the stopped screencast.
1700        stream_id: u64,
1701    },
1702}
1703
1704impl From<Duration> for Timestamp {
1705    fn from(value: Duration) -> Self {
1706        Timestamp {
1707            secs: value.as_secs(),
1708            nanos: value.subsec_nanos(),
1709        }
1710    }
1711}
1712
1713impl From<Timestamp> for Duration {
1714    fn from(value: Timestamp) -> Self {
1715        Duration::new(value.secs, value.nanos)
1716    }
1717}
1718
1719impl FromStr for WorkspaceReferenceArg {
1720    type Err = &'static str;
1721
1722    fn from_str(s: &str) -> Result<Self, Self::Err> {
1723        let reference = if let Ok(index) = s.parse::<i32>() {
1724            if let Ok(idx) = u8::try_from(index) {
1725                Self::Index(idx)
1726            } else {
1727                return Err("workspace index must be between 0 and 255");
1728            }
1729        } else {
1730            Self::Name(s.to_string())
1731        };
1732
1733        Ok(reference)
1734    }
1735}
1736
1737impl FromStr for SizeChange {
1738    type Err = &'static str;
1739
1740    fn from_str(s: &str) -> Result<Self, Self::Err> {
1741        match s.split_once('%') {
1742            Some((value, empty)) => {
1743                if !empty.is_empty() {
1744                    return Err("trailing characters after '%' are not allowed");
1745                }
1746
1747                match value.bytes().next() {
1748                    Some(b'-' | b'+') => {
1749                        let value = value.parse().map_err(|_| "error parsing value")?;
1750                        Ok(Self::AdjustProportion(value))
1751                    }
1752                    Some(_) => {
1753                        let value = value.parse().map_err(|_| "error parsing value")?;
1754                        Ok(Self::SetProportion(value))
1755                    }
1756                    None => Err("value is missing"),
1757                }
1758            }
1759            None => {
1760                let value = s;
1761                match value.bytes().next() {
1762                    Some(b'-' | b'+') => {
1763                        let value = value.parse().map_err(|_| "error parsing value")?;
1764                        Ok(Self::AdjustFixed(value))
1765                    }
1766                    Some(_) => {
1767                        let value = value.parse().map_err(|_| "error parsing value")?;
1768                        Ok(Self::SetFixed(value))
1769                    }
1770                    None => Err("value is missing"),
1771                }
1772            }
1773        }
1774    }
1775}
1776
1777impl FromStr for PositionChange {
1778    type Err = &'static str;
1779
1780    fn from_str(s: &str) -> Result<Self, Self::Err> {
1781        match s.split_once('%') {
1782            Some((value, empty)) => {
1783                if !empty.is_empty() {
1784                    return Err("trailing characters after '%' are not allowed");
1785                }
1786
1787                match value.bytes().next() {
1788                    Some(b'-' | b'+') => {
1789                        let value = value.parse().map_err(|_| "error parsing value")?;
1790                        Ok(Self::AdjustProportion(value))
1791                    }
1792                    Some(_) => {
1793                        let value = value.parse().map_err(|_| "error parsing value")?;
1794                        Ok(Self::SetProportion(value))
1795                    }
1796                    None => Err("value is missing"),
1797                }
1798            }
1799            None => {
1800                let value = s;
1801                match value.bytes().next() {
1802                    Some(b'-' | b'+') => {
1803                        let value = value.parse().map_err(|_| "error parsing value")?;
1804                        Ok(Self::AdjustFixed(value))
1805                    }
1806                    Some(_) => {
1807                        let value = value.parse().map_err(|_| "error parsing value")?;
1808                        Ok(Self::SetFixed(value))
1809                    }
1810                    None => Err("value is missing"),
1811                }
1812            }
1813        }
1814    }
1815}
1816
1817impl FromStr for LayoutSwitchTarget {
1818    type Err = &'static str;
1819
1820    fn from_str(s: &str) -> Result<Self, Self::Err> {
1821        match s {
1822            "next" => Ok(Self::Next),
1823            "prev" => Ok(Self::Prev),
1824            other => match other.parse() {
1825                Ok(layout) => Ok(Self::Index(layout)),
1826                _ => Err(r#"invalid layout action, can be "next", "prev" or a layout index"#),
1827            },
1828        }
1829    }
1830}
1831
1832impl FromStr for ColumnDisplay {
1833    type Err = &'static str;
1834
1835    fn from_str(s: &str) -> Result<Self, Self::Err> {
1836        match s {
1837            "normal" => Ok(Self::Normal),
1838            "tabbed" => Ok(Self::Tabbed),
1839            _ => Err(r#"invalid column display, can be "normal" or "tabbed""#),
1840        }
1841    }
1842}
1843
1844impl FromStr for Transform {
1845    type Err = &'static str;
1846
1847    fn from_str(s: &str) -> Result<Self, Self::Err> {
1848        match s {
1849            "normal" => Ok(Self::Normal),
1850            "90" => Ok(Self::_90),
1851            "180" => Ok(Self::_180),
1852            "270" => Ok(Self::_270),
1853            "flipped" => Ok(Self::Flipped),
1854            "flipped-90" => Ok(Self::Flipped90),
1855            "flipped-180" => Ok(Self::Flipped180),
1856            "flipped-270" => Ok(Self::Flipped270),
1857            _ => Err(concat!(
1858                r#"invalid transform, can be "90", "180", "270", "#,
1859                r#""flipped", "flipped-90", "flipped-180" or "flipped-270""#
1860            )),
1861        }
1862    }
1863}
1864
1865impl FromStr for ModeToSet {
1866    type Err = &'static str;
1867
1868    fn from_str(s: &str) -> Result<Self, Self::Err> {
1869        if s.eq_ignore_ascii_case("auto") {
1870            return Ok(Self::Automatic);
1871        }
1872
1873        let mode = s.parse()?;
1874        Ok(Self::Specific(mode))
1875    }
1876}
1877
1878impl FromStr for ConfiguredMode {
1879    type Err = &'static str;
1880
1881    fn from_str(s: &str) -> Result<Self, Self::Err> {
1882        let Some((width, rest)) = s.split_once('x') else {
1883            return Err("no 'x' separator found");
1884        };
1885
1886        let (height, refresh) = match rest.split_once('@') {
1887            Some((height, refresh)) => (height, Some(refresh)),
1888            None => (rest, None),
1889        };
1890
1891        let width = width.parse().map_err(|_| "error parsing width")?;
1892        let height = height.parse().map_err(|_| "error parsing height")?;
1893        let refresh = refresh
1894            .map(str::parse)
1895            .transpose()
1896            .map_err(|_| "error parsing refresh rate")?;
1897
1898        Ok(Self {
1899            width,
1900            height,
1901            refresh,
1902        })
1903    }
1904}
1905
1906impl FromStr for HSyncPolarity {
1907    type Err = &'static str;
1908
1909    fn from_str(s: &str) -> Result<Self, Self::Err> {
1910        match s {
1911            "+hsync" => Ok(Self::PHSync),
1912            "-hsync" => Ok(Self::NHSync),
1913            _ => Err(r#"invalid horizontal sync polarity, can be "+hsync" or "-hsync"#),
1914        }
1915    }
1916}
1917
1918impl FromStr for VSyncPolarity {
1919    type Err = &'static str;
1920
1921    fn from_str(s: &str) -> Result<Self, Self::Err> {
1922        match s {
1923            "+vsync" => Ok(Self::PVSync),
1924            "-vsync" => Ok(Self::NVSync),
1925            _ => Err(r#"invalid vertical sync polarity, can be "+vsync" or "-vsync"#),
1926        }
1927    }
1928}
1929
1930impl FromStr for ScaleToSet {
1931    type Err = &'static str;
1932
1933    fn from_str(s: &str) -> Result<Self, Self::Err> {
1934        if s.eq_ignore_ascii_case("auto") {
1935            return Ok(Self::Automatic);
1936        }
1937
1938        let scale = s.parse().map_err(|_| "error parsing scale")?;
1939        Ok(Self::Specific(scale))
1940    }
1941}
1942
1943macro_rules! ensure {
1944    ($cond:expr, $fmt:literal $($arg:tt)* ) => {
1945        if !$cond {
1946            return Err(format!($fmt $($arg)*));
1947        }
1948    };
1949}
1950
1951impl OutputAction {
1952    /// Validates some required constraints on the modeline and custom mode.
1953    pub fn validate(&self) -> Result<(), String> {
1954        match self {
1955            OutputAction::Modeline {
1956                hdisplay,
1957                hsync_start,
1958                hsync_end,
1959                htotal,
1960                vdisplay,
1961                vsync_start,
1962                vsync_end,
1963                vtotal,
1964                ..
1965            } => {
1966                ensure!(
1967                    hdisplay < hsync_start,
1968                    "hdisplay {} must be < hsync_start {}",
1969                    hdisplay,
1970                    hsync_start
1971                );
1972                ensure!(
1973                    hsync_start < hsync_end,
1974                    "hsync_start {} must be < hsync_end {}",
1975                    hsync_start,
1976                    hsync_end
1977                );
1978                ensure!(
1979                    hsync_end < htotal,
1980                    "hsync_end {} must be < htotal {}",
1981                    hsync_end,
1982                    htotal
1983                );
1984                ensure!(0 < *htotal, "htotal {} must be > 0", htotal);
1985                ensure!(
1986                    vdisplay < vsync_start,
1987                    "vdisplay {} must be < vsync_start {}",
1988                    vdisplay,
1989                    vsync_start
1990                );
1991                ensure!(
1992                    vsync_start < vsync_end,
1993                    "vsync_start {} must be < vsync_end {}",
1994                    vsync_start,
1995                    vsync_end
1996                );
1997                ensure!(
1998                    vsync_end < vtotal,
1999                    "vsync_end {} must be < vtotal {}",
2000                    vsync_end,
2001                    vtotal
2002                );
2003                ensure!(0 < *vtotal, "vtotal {} must be > 0", vtotal);
2004                Ok(())
2005            }
2006            OutputAction::CustomMode {
2007                mode: ConfiguredMode { refresh, .. },
2008            } => {
2009                if refresh.is_none() {
2010                    return Err("refresh rate is required for custom modes".to_string());
2011                }
2012                if let Some(refresh) = refresh {
2013                    if *refresh <= 0. {
2014                        return Err(format!("custom mode refresh rate {refresh} must be > 0"));
2015                    }
2016                }
2017                Ok(())
2018            }
2019            _ => Ok(()),
2020        }
2021    }
2022}
2023
2024#[cfg(test)]
2025mod tests {
2026    use super::*;
2027
2028    #[test]
2029    fn parse_size_change() {
2030        assert_eq!(
2031            "10".parse::<SizeChange>().unwrap(),
2032            SizeChange::SetFixed(10),
2033        );
2034        assert_eq!(
2035            "+10".parse::<SizeChange>().unwrap(),
2036            SizeChange::AdjustFixed(10),
2037        );
2038        assert_eq!(
2039            "-10".parse::<SizeChange>().unwrap(),
2040            SizeChange::AdjustFixed(-10),
2041        );
2042        assert_eq!(
2043            "10%".parse::<SizeChange>().unwrap(),
2044            SizeChange::SetProportion(10.),
2045        );
2046        assert_eq!(
2047            "+10%".parse::<SizeChange>().unwrap(),
2048            SizeChange::AdjustProportion(10.),
2049        );
2050        assert_eq!(
2051            "-10%".parse::<SizeChange>().unwrap(),
2052            SizeChange::AdjustProportion(-10.),
2053        );
2054
2055        assert!("-".parse::<SizeChange>().is_err());
2056        assert!("10% ".parse::<SizeChange>().is_err());
2057    }
2058
2059    #[test]
2060    fn parse_position_change() {
2061        assert_eq!(
2062            "10".parse::<PositionChange>().unwrap(),
2063            PositionChange::SetFixed(10.),
2064        );
2065        assert_eq!(
2066            "+10".parse::<PositionChange>().unwrap(),
2067            PositionChange::AdjustFixed(10.),
2068        );
2069        assert_eq!(
2070            "-10".parse::<PositionChange>().unwrap(),
2071            PositionChange::AdjustFixed(-10.),
2072        );
2073
2074        assert_eq!(
2075            "10%".parse::<PositionChange>().unwrap(),
2076            PositionChange::SetProportion(10.)
2077        );
2078        assert_eq!(
2079            "+10%".parse::<PositionChange>().unwrap(),
2080            PositionChange::AdjustProportion(10.)
2081        );
2082        assert_eq!(
2083            "-10%".parse::<PositionChange>().unwrap(),
2084            PositionChange::AdjustProportion(-10.)
2085        );
2086        assert!("-".parse::<PositionChange>().is_err());
2087        assert!("10% ".parse::<PositionChange>().is_err());
2088    }
2089}