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        /// Path of a new config file to load.
941        ///
942        /// If unset, reloads the current config file.
943        #[cfg_attr(feature = "clap", arg(long))]
944        path: Option<String>,
945    },
946}
947
948/// Change in window or column size.
949#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
950#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
951pub enum SizeChange {
952    /// Set the size in logical pixels.
953    SetFixed(i32),
954    /// Set the size as a proportion of the working area.
955    SetProportion(f64),
956    /// Add or subtract to the current size in logical pixels.
957    AdjustFixed(i32),
958    /// Add or subtract to the current size as a proportion of the working area.
959    AdjustProportion(f64),
960}
961
962/// Change in floating window position.
963#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
964#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
965pub enum PositionChange {
966    /// Set the position in logical pixels.
967    SetFixed(f64),
968    /// Set the position as a proportion of the working area.
969    SetProportion(f64),
970    /// Add or subtract to the current position in logical pixels.
971    AdjustFixed(f64),
972    /// Add or subtract to the current position as a proportion of the working area.
973    AdjustProportion(f64),
974}
975
976/// Workspace reference (id, index or name) to operate on.
977#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
978#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
979pub enum WorkspaceReferenceArg {
980    /// Id of the workspace.
981    Id(u64),
982    /// Index of the workspace.
983    Index(u8),
984    /// Name of the workspace.
985    Name(String),
986}
987
988/// Layout to switch to.
989#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
990#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
991pub enum LayoutSwitchTarget {
992    /// The next configured layout.
993    Next,
994    /// The previous configured layout.
995    Prev,
996    /// The specific layout by index.
997    Index(u8),
998}
999
1000/// How windows display in a column.
1001#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1002#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1003pub enum ColumnDisplay {
1004    /// Windows are tiled vertically across the working area height.
1005    Normal,
1006    /// Windows are in tabs.
1007    Tabbed,
1008}
1009
1010/// Output actions that niri can perform.
1011// Variants in this enum should match the spelling of the ones in niri-config. Most thigs from
1012// niri-config should be present here.
1013#[derive(Serialize, Deserialize, Debug, Clone)]
1014#[cfg_attr(feature = "clap", derive(clap::Parser))]
1015#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
1016#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
1017#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1018pub enum OutputAction {
1019    /// Turn off the output.
1020    Off,
1021    /// Turn on the output.
1022    On,
1023    /// Set the output mode.
1024    Mode {
1025        /// Mode to set, or "auto" for automatic selection.
1026        ///
1027        /// Run `niri msg outputs` to see the available modes.
1028        #[cfg_attr(feature = "clap", arg())]
1029        mode: ModeToSet,
1030    },
1031    /// Set a custom output mode.
1032    CustomMode {
1033        /// Custom mode to set.
1034        #[cfg_attr(feature = "clap", arg())]
1035        mode: ConfiguredMode,
1036    },
1037    /// Set a custom VESA CVT modeline.
1038    #[cfg_attr(feature = "clap", arg())]
1039    Modeline {
1040        /// The rate at which pixels are drawn in MHz.
1041        #[cfg_attr(feature = "clap", arg())]
1042        clock: f64,
1043        /// Horizontal active pixels.
1044        #[cfg_attr(feature = "clap", arg())]
1045        hdisplay: u16,
1046        /// Horizontal sync pulse start position in pixels.
1047        #[cfg_attr(feature = "clap", arg())]
1048        hsync_start: u16,
1049        /// Horizontal sync pulse end position in pixels.
1050        #[cfg_attr(feature = "clap", arg())]
1051        hsync_end: u16,
1052        /// Total horizontal number of pixels before resetting the horizontal drawing position to
1053        /// zero.
1054        #[cfg_attr(feature = "clap", arg())]
1055        htotal: u16,
1056
1057        /// Vertical active pixels.
1058        #[cfg_attr(feature = "clap", arg())]
1059        vdisplay: u16,
1060        /// Vertical sync pulse start position in pixels.
1061        #[cfg_attr(feature = "clap", arg())]
1062        vsync_start: u16,
1063        /// Vertical sync pulse end position in pixels.
1064        #[cfg_attr(feature = "clap", arg())]
1065        vsync_end: u16,
1066        /// Total vertical number of pixels before resetting the vertical drawing position to zero.
1067        #[cfg_attr(feature = "clap", arg())]
1068        vtotal: u16,
1069        /// Horizontal sync polarity: "+hsync" or "-hsync".
1070        #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
1071        hsync_polarity: HSyncPolarity,
1072        /// Vertical sync polarity: "+vsync" or "-vsync".
1073        #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
1074        vsync_polarity: VSyncPolarity,
1075    },
1076    /// Set the output scale.
1077    Scale {
1078        /// Scale factor to set, or "auto" for automatic selection.
1079        #[cfg_attr(feature = "clap", arg())]
1080        scale: ScaleToSet,
1081    },
1082    /// Set the output transform.
1083    Transform {
1084        /// Transform to set, counter-clockwise.
1085        #[cfg_attr(feature = "clap", arg())]
1086        transform: Transform,
1087    },
1088    /// Set the output position.
1089    Position {
1090        /// Position to set, or "auto" for automatic selection.
1091        #[cfg_attr(feature = "clap", command(subcommand))]
1092        position: PositionToSet,
1093    },
1094    /// Set the variable refresh rate mode.
1095    Vrr {
1096        /// Variable refresh rate mode to set.
1097        #[cfg_attr(feature = "clap", command(flatten))]
1098        vrr: VrrToSet,
1099    },
1100}
1101
1102/// Output mode to set.
1103#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1104#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1105pub enum ModeToSet {
1106    /// Niri will pick the mode automatically.
1107    Automatic,
1108    /// Specific mode.
1109    Specific(ConfiguredMode),
1110}
1111
1112/// Output mode as set in the config file.
1113#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1114#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1115pub struct ConfiguredMode {
1116    /// Width in physical pixels.
1117    pub width: u16,
1118    /// Height in physical pixels.
1119    pub height: u16,
1120    /// Refresh rate.
1121    pub refresh: Option<f64>,
1122}
1123
1124/// Modeline horizontal syncing polarity.
1125#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1126#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1127pub enum HSyncPolarity {
1128    /// Positive polarity.
1129    PHSync,
1130    /// Negative polarity.
1131    NHSync,
1132}
1133
1134/// Modeline vertical syncing polarity.
1135#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1136#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1137pub enum VSyncPolarity {
1138    /// Positive polarity.
1139    PVSync,
1140    /// Negative polarity.
1141    NVSync,
1142}
1143
1144/// Output scale to set.
1145#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1146#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1147pub enum ScaleToSet {
1148    /// Niri will pick the scale automatically.
1149    Automatic,
1150    /// Specific scale.
1151    Specific(f64),
1152}
1153
1154/// Output position to set.
1155#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1156#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
1157#[cfg_attr(feature = "clap", command(subcommand_value_name = "POSITION"))]
1158#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Position Values"))]
1159#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1160pub enum PositionToSet {
1161    /// Position the output automatically.
1162    #[cfg_attr(feature = "clap", command(name = "auto"))]
1163    Automatic,
1164    /// Set a specific position.
1165    #[cfg_attr(feature = "clap", command(name = "set"))]
1166    Specific(ConfiguredPosition),
1167}
1168
1169/// Output position as set in the config file.
1170#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1171#[cfg_attr(feature = "clap", derive(clap::Args))]
1172#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1173pub struct ConfiguredPosition {
1174    /// Logical X position.
1175    pub x: i32,
1176    /// Logical Y position.
1177    pub y: i32,
1178}
1179
1180/// Output VRR to set.
1181#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
1182#[cfg_attr(feature = "clap", derive(clap::Args))]
1183#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1184pub struct VrrToSet {
1185    /// Whether to enable variable refresh rate.
1186    #[cfg_attr(
1187        feature = "clap",
1188        arg(
1189            value_name = "ON|OFF",
1190            action = clap::ArgAction::Set,
1191            value_parser = clap::builder::BoolishValueParser::new(),
1192            hide_possible_values = true,
1193        ),
1194    )]
1195    pub vrr: bool,
1196    /// Only enable when the output shows a window matching the variable-refresh-rate window rule.
1197    #[cfg_attr(feature = "clap", arg(long))]
1198    pub on_demand: bool,
1199}
1200
1201/// Connected output.
1202#[derive(Debug, Serialize, Deserialize, Clone)]
1203#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1204pub struct Output {
1205    /// Name of the output.
1206    pub name: String,
1207    /// Textual description of the manufacturer.
1208    pub make: String,
1209    /// Textual description of the model.
1210    pub model: String,
1211    /// Serial of the output, if known.
1212    pub serial: Option<String>,
1213    /// Physical width and height of the output in millimeters, if known.
1214    pub physical_size: Option<(u32, u32)>,
1215    /// Available modes for the output.
1216    pub modes: Vec<Mode>,
1217    /// Index of the current mode in [`Self::modes`].
1218    ///
1219    /// `None` if the output is disabled.
1220    pub current_mode: Option<usize>,
1221    /// Whether the current_mode is a custom mode.
1222    pub is_custom_mode: bool,
1223    /// Whether the output supports variable refresh rate.
1224    pub vrr_supported: bool,
1225    /// Whether variable refresh rate is enabled on the output.
1226    pub vrr_enabled: bool,
1227    /// Logical output information.
1228    ///
1229    /// `None` if the output is not mapped to any logical output (for example, if it is disabled).
1230    pub logical: Option<LogicalOutput>,
1231}
1232
1233/// Output mode.
1234#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
1235#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1236pub struct Mode {
1237    /// Width in physical pixels.
1238    pub width: u16,
1239    /// Height in physical pixels.
1240    pub height: u16,
1241    /// Refresh rate in millihertz.
1242    pub refresh_rate: u32,
1243    /// Whether this mode is preferred by the monitor.
1244    pub is_preferred: bool,
1245}
1246
1247/// Logical output in the compositor's coordinate space.
1248#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
1249#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1250pub struct LogicalOutput {
1251    /// Logical X position.
1252    pub x: i32,
1253    /// Logical Y position.
1254    pub y: i32,
1255    /// Width in logical pixels.
1256    pub width: u32,
1257    /// Height in logical pixels.
1258    pub height: u32,
1259    /// Scale factor.
1260    pub scale: f64,
1261    /// Transform.
1262    pub transform: Transform,
1263}
1264
1265/// Output transform, which goes counter-clockwise.
1266#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1267#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
1268#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1269pub enum Transform {
1270    /// Untransformed.
1271    Normal,
1272    /// Rotated by 90°.
1273    #[serde(rename = "90")]
1274    _90,
1275    /// Rotated by 180°.
1276    #[serde(rename = "180")]
1277    _180,
1278    /// Rotated by 270°.
1279    #[serde(rename = "270")]
1280    _270,
1281    /// Flipped horizontally.
1282    Flipped,
1283    /// Rotated by 90° and flipped horizontally.
1284    #[cfg_attr(feature = "clap", value(name("flipped-90")))]
1285    Flipped90,
1286    /// Flipped vertically.
1287    #[cfg_attr(feature = "clap", value(name("flipped-180")))]
1288    Flipped180,
1289    /// Rotated by 270° and flipped horizontally.
1290    #[cfg_attr(feature = "clap", value(name("flipped-270")))]
1291    Flipped270,
1292}
1293
1294/// Toplevel window.
1295#[derive(Serialize, Deserialize, Debug, Clone)]
1296#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1297pub struct Window {
1298    /// Unique id of this window.
1299    ///
1300    /// This id remains constant while this window is open.
1301    ///
1302    /// Do not assume that window ids will always increase without wrapping, or start at 1. That is
1303    /// an implementation detail subject to change. For example, ids may change to be randomly
1304    /// generated for each new window.
1305    pub id: u64,
1306    /// Title, if set.
1307    pub title: Option<String>,
1308    /// Application ID, if set.
1309    pub app_id: Option<String>,
1310    /// Process ID that created the Wayland connection for this window, if known.
1311    ///
1312    /// Currently, windows created by xdg-desktop-portal-gnome will have a `None` PID, but this may
1313    /// change in the future.
1314    pub pid: Option<i32>,
1315    /// Id of the workspace this window is on, if any.
1316    pub workspace_id: Option<u64>,
1317    /// Whether this window is currently focused.
1318    ///
1319    /// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
1320    pub is_focused: bool,
1321    /// Whether this window is currently floating.
1322    ///
1323    /// If the window isn't floating then it is in the tiling layout.
1324    pub is_floating: bool,
1325    /// Whether this window requests your attention.
1326    pub is_urgent: bool,
1327    /// Position- and size-related properties of the window.
1328    pub layout: WindowLayout,
1329    /// Timestamp when the window was most recently focused.
1330    ///
1331    /// This timestamp is intended for most-recently-used window switchers, i.e. Alt-Tab. It only
1332    /// updates after some debounce time so that quick window switching doesn't mark intermediate
1333    /// windows as recently focused.
1334    ///
1335    /// The timestamp comes from the monotonic clock.
1336    pub focus_timestamp: Option<Timestamp>,
1337}
1338
1339/// A moment in time.
1340#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1341#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1342pub struct Timestamp {
1343    /// Number of whole seconds.
1344    pub secs: u64,
1345    /// Fractional part of the timestamp in nanoseconds (10<sup>-9</sup> seconds).
1346    pub nanos: u32,
1347}
1348
1349/// Position- and size-related properties of a [`Window`].
1350///
1351/// Optional properties will be unset for some windows, do not rely on them being present. Whether
1352/// some optional properties are present or absent for certain window types may change across niri
1353/// releases.
1354///
1355/// All sizes and positions are in *logical pixels* unless stated otherwise. Logical sizes may be
1356/// fractional. For example, at 1.25 monitor scale, a 2-physical-pixel-wide window border is 1.6
1357/// logical pixels wide.
1358///
1359/// This struct contains positions and sizes both for full tiles ([`Self::tile_size`],
1360/// [`Self::tile_pos_in_workspace_view`]) and the window geometry ([`Self::window_size`],
1361/// [`Self::window_offset_in_tile`]). For visual displays, use the tile properties, as they
1362/// correspond to what the user visually considers "window". The window properties on the other
1363/// hand are mainly useful when you need to know the underlying Wayland window sizes, e.g. for
1364/// application debugging.
1365#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1366#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1367pub struct WindowLayout {
1368    /// Location of a tiled window within a workspace: (column index, tile index in column).
1369    ///
1370    /// The indices are 1-based, i.e. the leftmost column is at index 1 and the topmost tile in a
1371    /// column is at index 1. This is consistent with [`Action::FocusColumn`] and
1372    /// [`Action::FocusWindowInColumn`].
1373    pub pos_in_scrolling_layout: Option<(usize, usize)>,
1374    /// Size of the tile this window is in, including decorations like borders.
1375    pub tile_size: (f64, f64),
1376    /// Size of the window's visual geometry itself.
1377    ///
1378    /// Does not include niri decorations like borders.
1379    ///
1380    /// Currently, Wayland toplevel windows can only be integer-sized in logical pixels, even
1381    /// though it doesn't necessarily align to physical pixels.
1382    pub window_size: (i32, i32),
1383    /// Tile position within the current view of the workspace.
1384    ///
1385    /// This is the same "workspace view" as in gradients' `relative-to` in the niri config.
1386    pub tile_pos_in_workspace_view: Option<(f64, f64)>,
1387    /// Location of the window's visual geometry within its tile.
1388    ///
1389    /// This includes things like border sizes. For fullscreened fixed-size windows this includes
1390    /// the distance from the corner of the black backdrop to the corner of the (centered) window
1391    /// contents.
1392    pub window_offset_in_tile: (f64, f64),
1393}
1394
1395/// Output configuration change result.
1396#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1397#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1398pub enum OutputConfigChanged {
1399    /// The target output was connected and the change was applied.
1400    Applied,
1401    /// The target output was not found, the change will be applied when it is connected.
1402    OutputWasMissing,
1403}
1404
1405/// A workspace.
1406#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1407#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1408pub struct Workspace {
1409    /// Unique id of this workspace.
1410    ///
1411    /// This id remains constant regardless of the workspace moving around and across monitors.
1412    ///
1413    /// Do not assume that workspace ids will always increase without wrapping, or start at 1. That
1414    /// is an implementation detail subject to change. For example, ids may change to be randomly
1415    /// generated for each new workspace.
1416    pub id: u64,
1417    /// Index of the workspace on its monitor.
1418    ///
1419    /// This is the same index you can use for requests like `niri msg action focus-workspace`.
1420    ///
1421    /// This index *will change* as you move and re-order workspace. It is merely the workspace's
1422    /// current position on its monitor. Workspaces on different monitors can have the same index.
1423    ///
1424    /// If you need a unique workspace id that doesn't change, see [`Self::id`].
1425    pub idx: u8,
1426    /// Optional name of the workspace.
1427    pub name: Option<String>,
1428    /// Name of the output that the workspace is on.
1429    ///
1430    /// Can be `None` if no outputs are currently connected.
1431    pub output: Option<String>,
1432    /// Whether the workspace currently has an urgent window in its output.
1433    pub is_urgent: bool,
1434    /// Whether the workspace is currently active on its output.
1435    ///
1436    /// Every output has one active workspace, the one that is currently visible on that output.
1437    pub is_active: bool,
1438    /// Whether the workspace is currently focused.
1439    ///
1440    /// There's only one focused workspace across all outputs.
1441    pub is_focused: bool,
1442    /// Id of the active window on this workspace, if any.
1443    pub active_window_id: Option<u64>,
1444}
1445
1446/// Configured keyboard layouts.
1447#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1448#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1449pub struct KeyboardLayouts {
1450    /// XKB names of the configured layouts.
1451    pub names: Vec<String>,
1452    /// Index of the currently active layout in `names`.
1453    pub current_idx: u8,
1454}
1455
1456/// A layer-shell layer.
1457#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1458#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1459pub enum Layer {
1460    /// The background layer.
1461    Background,
1462    /// The bottom layer.
1463    Bottom,
1464    /// The top layer.
1465    Top,
1466    /// The overlay layer.
1467    Overlay,
1468}
1469
1470/// Keyboard interactivity modes for a layer-shell surface.
1471#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1472#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1473pub enum LayerSurfaceKeyboardInteractivity {
1474    /// Surface cannot receive keyboard focus.
1475    None,
1476    /// Surface receives keyboard focus whenever possible.
1477    Exclusive,
1478    /// Surface receives keyboard focus on demand, e.g. when clicked.
1479    OnDemand,
1480}
1481
1482/// A layer-shell surface.
1483#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1484#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1485pub struct LayerSurface {
1486    /// Namespace provided by the layer-shell client.
1487    pub namespace: String,
1488    /// Name of the output the surface is on.
1489    pub output: String,
1490    /// Layer that the surface is on.
1491    pub layer: Layer,
1492    /// The surface's keyboard interactivity mode.
1493    pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
1494}
1495
1496/// A screencast.
1497#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1498#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1499pub struct Cast {
1500    /// Stream ID of the screencast that uniquely identifies it.
1501    pub stream_id: u64,
1502    /// Session ID of the screencast.
1503    ///
1504    /// A session can have multiple screencast streams. Then multiple `Cast`s will have the same
1505    /// `session_id`. Though, usually there's only one stream per session.
1506    ///
1507    /// Do not confuse `session_id` with [`stream_id`](Self::stream_id).
1508    pub session_id: u64,
1509    /// Kind of this screencast.
1510    pub kind: CastKind,
1511    /// Target being captured.
1512    pub target: CastTarget,
1513    /// Whether this is a Dynamic Cast Target screencast.
1514    ///
1515    /// Meaning that actions like `SetDynamicCastWindow` will act on this screencast.
1516    ///
1517    /// Keep in mind that the target can change even if this is `false`.
1518    pub is_dynamic_target: bool,
1519    /// Whether the cast is currently streaming frames.
1520    ///
1521    /// This can be `false` for example when switching away to a different scene in OBS, which
1522    /// pauses the stream.
1523    pub is_active: bool,
1524    /// Process ID of the screencast consumer, if known.
1525    ///
1526    /// Currently, only wlr-screencopy screencasts can have a pid.
1527    pub pid: Option<i32>,
1528    /// PipeWire node ID of the screencast stream.
1529    ///
1530    /// This is `None` for wlr-screencopy casts, and also for PipeWire casts before the node is
1531    /// created (when the cast is just starting up).
1532    pub pw_node_id: Option<u32>,
1533}
1534
1535/// Kind of screencast.
1536#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
1537#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1538pub enum CastKind {
1539    /// PipeWire screencast, typically via xdg-desktop-portal-gnome.
1540    PipeWire,
1541    /// wlr-screencopy protocol screencast.
1542    ///
1543    /// Tools like wf-recorder, and the xdg-desktop-portal-wlr portal.
1544    ///
1545    /// Only wlr-screencopy with damage tracking is reported here. Screencopy without damage is
1546    /// treated as a regular screenshot and not reported as a screencast.
1547    WlrScreencopy,
1548}
1549
1550/// Target of a screencast.
1551#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1552#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1553pub enum CastTarget {
1554    /// The target is not yet set, or was cleared.
1555    Nothing {},
1556    /// Casting an output.
1557    Output {
1558        /// Name of the screencasted output.
1559        name: String,
1560    },
1561    /// Casting a window.
1562    Window {
1563        /// ID of the screencasted window.
1564        id: u64,
1565    },
1566}
1567
1568/// A compositor event.
1569#[derive(Serialize, Deserialize, Debug, Clone)]
1570#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
1571pub enum Event {
1572    /// The workspace configuration has changed.
1573    WorkspacesChanged {
1574        /// The new workspace configuration.
1575        ///
1576        /// This configuration completely replaces the previous configuration. I.e. if any
1577        /// workspaces are missing from here, then they were deleted.
1578        workspaces: Vec<Workspace>,
1579    },
1580    /// The workspace urgency changed.
1581    WorkspaceUrgencyChanged {
1582        /// Id of the workspace.
1583        id: u64,
1584        /// Whether this workspace has an urgent window.
1585        urgent: bool,
1586    },
1587    /// A workspace was activated on an output.
1588    ///
1589    /// This doesn't always mean the workspace became focused, just that it's now the active
1590    /// workspace on its output. All other workspaces on the same output become inactive.
1591    WorkspaceActivated {
1592        /// Id of the newly active workspace.
1593        id: u64,
1594        /// Whether this workspace also became focused.
1595        ///
1596        /// If `true`, this is now the single focused workspace. All other workspaces are no longer
1597        /// focused, but they may remain active on their respective outputs.
1598        focused: bool,
1599    },
1600    /// An active window changed on a workspace.
1601    WorkspaceActiveWindowChanged {
1602        /// Id of the workspace on which the active window changed.
1603        workspace_id: u64,
1604        /// Id of the new active window, if any.
1605        active_window_id: Option<u64>,
1606    },
1607    /// The window configuration has changed.
1608    WindowsChanged {
1609        /// The new window configuration.
1610        ///
1611        /// This configuration completely replaces the previous configuration. I.e. if any windows
1612        /// are missing from here, then they were closed.
1613        windows: Vec<Window>,
1614    },
1615    /// A new toplevel window was opened, or an existing toplevel window changed.
1616    WindowOpenedOrChanged {
1617        /// The new or updated window.
1618        ///
1619        /// If the window is focused, all other windows are no longer focused.
1620        window: Window,
1621    },
1622    /// A toplevel window was closed.
1623    WindowClosed {
1624        /// Id of the removed window.
1625        id: u64,
1626    },
1627    /// Window focus changed.
1628    ///
1629    /// All other windows are no longer focused.
1630    WindowFocusChanged {
1631        /// Id of the newly focused window, or `None` if no window is now focused.
1632        id: Option<u64>,
1633    },
1634    /// Window focus timestamp changed.
1635    ///
1636    /// This event is separate from [`Event::WindowFocusChanged`] because the focus timestamp only
1637    /// updates after some debounce time so that quick window switching doesn't mark intermediate
1638    /// windows as recently focused.
1639    WindowFocusTimestampChanged {
1640        /// Id of the window.
1641        id: u64,
1642        /// The new focus timestamp.
1643        focus_timestamp: Option<Timestamp>,
1644    },
1645    /// Window urgency changed.
1646    WindowUrgencyChanged {
1647        /// Id of the window.
1648        id: u64,
1649        /// The new urgency state of the window.
1650        urgent: bool,
1651    },
1652    /// The layout of one or more windows has changed.
1653    WindowLayoutsChanged {
1654        /// Pairs consisting of a window id and new layout information for the window.
1655        changes: Vec<(u64, WindowLayout)>,
1656    },
1657    /// The configured keyboard layouts have changed.
1658    KeyboardLayoutsChanged {
1659        /// The new keyboard layout configuration.
1660        keyboard_layouts: KeyboardLayouts,
1661    },
1662    /// The keyboard layout switched.
1663    KeyboardLayoutSwitched {
1664        /// Index of the newly active layout.
1665        idx: u8,
1666    },
1667    /// The overview was opened or closed.
1668    OverviewOpenedOrClosed {
1669        /// The new state of the overview.
1670        is_open: bool,
1671    },
1672    /// The configuration was reloaded.
1673    ///
1674    /// You will always receive this event when connecting to the event stream, indicating the last
1675    /// config load attempt.
1676    ConfigLoaded {
1677        /// Whether the loading failed.
1678        ///
1679        /// For example, the config file couldn't be parsed.
1680        failed: bool,
1681    },
1682    /// A screenshot was captured.
1683    ScreenshotCaptured {
1684        /// The file path where the screenshot was saved, if it was written to disk.
1685        ///
1686        /// If `None`, the screenshot was either only copied to the clipboard, or the path couldn't
1687        /// be converted to a `String` (e.g. contained invalid UTF-8 bytes).
1688        path: Option<String>,
1689    },
1690    /// The screencasts have changed.
1691    CastsChanged {
1692        /// The new screencast information.
1693        ///
1694        /// This configuration completely replaces the previous configuration. I.e. if any casts
1695        /// are missing from here, then they were stopped.
1696        casts: Vec<Cast>,
1697    },
1698    /// A screencast started, or an existing cast changed.
1699    CastStartedOrChanged {
1700        /// The cast that started or changed.
1701        cast: Cast,
1702    },
1703    /// A screencast stopped.
1704    CastStopped {
1705        /// Stream ID of the stopped screencast.
1706        stream_id: u64,
1707    },
1708}
1709
1710impl From<Duration> for Timestamp {
1711    fn from(value: Duration) -> Self {
1712        Timestamp {
1713            secs: value.as_secs(),
1714            nanos: value.subsec_nanos(),
1715        }
1716    }
1717}
1718
1719impl From<Timestamp> for Duration {
1720    fn from(value: Timestamp) -> Self {
1721        Duration::new(value.secs, value.nanos)
1722    }
1723}
1724
1725impl FromStr for WorkspaceReferenceArg {
1726    type Err = &'static str;
1727
1728    fn from_str(s: &str) -> Result<Self, Self::Err> {
1729        let reference = if let Ok(index) = s.parse::<i32>() {
1730            if let Ok(idx) = u8::try_from(index) {
1731                Self::Index(idx)
1732            } else {
1733                return Err("workspace index must be between 0 and 255");
1734            }
1735        } else {
1736            Self::Name(s.to_string())
1737        };
1738
1739        Ok(reference)
1740    }
1741}
1742
1743impl FromStr for SizeChange {
1744    type Err = &'static str;
1745
1746    fn from_str(s: &str) -> Result<Self, Self::Err> {
1747        match s.split_once('%') {
1748            Some((value, empty)) => {
1749                if !empty.is_empty() {
1750                    return Err("trailing characters after '%' are not allowed");
1751                }
1752
1753                match value.bytes().next() {
1754                    Some(b'-' | b'+') => {
1755                        let value = value.parse().map_err(|_| "error parsing value")?;
1756                        Ok(Self::AdjustProportion(value))
1757                    }
1758                    Some(_) => {
1759                        let value = value.parse().map_err(|_| "error parsing value")?;
1760                        Ok(Self::SetProportion(value))
1761                    }
1762                    None => Err("value is missing"),
1763                }
1764            }
1765            None => {
1766                let value = s;
1767                match value.bytes().next() {
1768                    Some(b'-' | b'+') => {
1769                        let value = value.parse().map_err(|_| "error parsing value")?;
1770                        Ok(Self::AdjustFixed(value))
1771                    }
1772                    Some(_) => {
1773                        let value = value.parse().map_err(|_| "error parsing value")?;
1774                        Ok(Self::SetFixed(value))
1775                    }
1776                    None => Err("value is missing"),
1777                }
1778            }
1779        }
1780    }
1781}
1782
1783impl FromStr for PositionChange {
1784    type Err = &'static str;
1785
1786    fn from_str(s: &str) -> Result<Self, Self::Err> {
1787        match s.split_once('%') {
1788            Some((value, empty)) => {
1789                if !empty.is_empty() {
1790                    return Err("trailing characters after '%' are not allowed");
1791                }
1792
1793                match value.bytes().next() {
1794                    Some(b'-' | b'+') => {
1795                        let value = value.parse().map_err(|_| "error parsing value")?;
1796                        Ok(Self::AdjustProportion(value))
1797                    }
1798                    Some(_) => {
1799                        let value = value.parse().map_err(|_| "error parsing value")?;
1800                        Ok(Self::SetProportion(value))
1801                    }
1802                    None => Err("value is missing"),
1803                }
1804            }
1805            None => {
1806                let value = s;
1807                match value.bytes().next() {
1808                    Some(b'-' | b'+') => {
1809                        let value = value.parse().map_err(|_| "error parsing value")?;
1810                        Ok(Self::AdjustFixed(value))
1811                    }
1812                    Some(_) => {
1813                        let value = value.parse().map_err(|_| "error parsing value")?;
1814                        Ok(Self::SetFixed(value))
1815                    }
1816                    None => Err("value is missing"),
1817                }
1818            }
1819        }
1820    }
1821}
1822
1823impl FromStr for LayoutSwitchTarget {
1824    type Err = &'static str;
1825
1826    fn from_str(s: &str) -> Result<Self, Self::Err> {
1827        match s {
1828            "next" => Ok(Self::Next),
1829            "prev" => Ok(Self::Prev),
1830            other => match other.parse() {
1831                Ok(layout) => Ok(Self::Index(layout)),
1832                _ => Err(r#"invalid layout action, can be "next", "prev" or a layout index"#),
1833            },
1834        }
1835    }
1836}
1837
1838impl FromStr for ColumnDisplay {
1839    type Err = &'static str;
1840
1841    fn from_str(s: &str) -> Result<Self, Self::Err> {
1842        match s {
1843            "normal" => Ok(Self::Normal),
1844            "tabbed" => Ok(Self::Tabbed),
1845            _ => Err(r#"invalid column display, can be "normal" or "tabbed""#),
1846        }
1847    }
1848}
1849
1850impl FromStr for Transform {
1851    type Err = &'static str;
1852
1853    fn from_str(s: &str) -> Result<Self, Self::Err> {
1854        match s {
1855            "normal" => Ok(Self::Normal),
1856            "90" => Ok(Self::_90),
1857            "180" => Ok(Self::_180),
1858            "270" => Ok(Self::_270),
1859            "flipped" => Ok(Self::Flipped),
1860            "flipped-90" => Ok(Self::Flipped90),
1861            "flipped-180" => Ok(Self::Flipped180),
1862            "flipped-270" => Ok(Self::Flipped270),
1863            _ => Err(concat!(
1864                r#"invalid transform, can be "90", "180", "270", "#,
1865                r#""flipped", "flipped-90", "flipped-180" or "flipped-270""#
1866            )),
1867        }
1868    }
1869}
1870
1871impl FromStr for ModeToSet {
1872    type Err = &'static str;
1873
1874    fn from_str(s: &str) -> Result<Self, Self::Err> {
1875        if s.eq_ignore_ascii_case("auto") {
1876            return Ok(Self::Automatic);
1877        }
1878
1879        let mode = s.parse()?;
1880        Ok(Self::Specific(mode))
1881    }
1882}
1883
1884impl FromStr for ConfiguredMode {
1885    type Err = &'static str;
1886
1887    fn from_str(s: &str) -> Result<Self, Self::Err> {
1888        let Some((width, rest)) = s.split_once('x') else {
1889            return Err("no 'x' separator found");
1890        };
1891
1892        let (height, refresh) = match rest.split_once('@') {
1893            Some((height, refresh)) => (height, Some(refresh)),
1894            None => (rest, None),
1895        };
1896
1897        let width = width.parse().map_err(|_| "error parsing width")?;
1898        let height = height.parse().map_err(|_| "error parsing height")?;
1899        let refresh = refresh
1900            .map(str::parse)
1901            .transpose()
1902            .map_err(|_| "error parsing refresh rate")?;
1903
1904        Ok(Self {
1905            width,
1906            height,
1907            refresh,
1908        })
1909    }
1910}
1911
1912impl FromStr for HSyncPolarity {
1913    type Err = &'static str;
1914
1915    fn from_str(s: &str) -> Result<Self, Self::Err> {
1916        match s {
1917            "+hsync" => Ok(Self::PHSync),
1918            "-hsync" => Ok(Self::NHSync),
1919            _ => Err(r#"invalid horizontal sync polarity, can be "+hsync" or "-hsync"#),
1920        }
1921    }
1922}
1923
1924impl FromStr for VSyncPolarity {
1925    type Err = &'static str;
1926
1927    fn from_str(s: &str) -> Result<Self, Self::Err> {
1928        match s {
1929            "+vsync" => Ok(Self::PVSync),
1930            "-vsync" => Ok(Self::NVSync),
1931            _ => Err(r#"invalid vertical sync polarity, can be "+vsync" or "-vsync"#),
1932        }
1933    }
1934}
1935
1936impl FromStr for ScaleToSet {
1937    type Err = &'static str;
1938
1939    fn from_str(s: &str) -> Result<Self, Self::Err> {
1940        if s.eq_ignore_ascii_case("auto") {
1941            return Ok(Self::Automatic);
1942        }
1943
1944        let scale = s.parse().map_err(|_| "error parsing scale")?;
1945        Ok(Self::Specific(scale))
1946    }
1947}
1948
1949macro_rules! ensure {
1950    ($cond:expr, $fmt:literal $($arg:tt)* ) => {
1951        if !$cond {
1952            return Err(format!($fmt $($arg)*));
1953        }
1954    };
1955}
1956
1957impl OutputAction {
1958    /// Validates some required constraints on the modeline and custom mode.
1959    pub fn validate(&self) -> Result<(), String> {
1960        match self {
1961            OutputAction::Modeline {
1962                hdisplay,
1963                hsync_start,
1964                hsync_end,
1965                htotal,
1966                vdisplay,
1967                vsync_start,
1968                vsync_end,
1969                vtotal,
1970                ..
1971            } => {
1972                ensure!(
1973                    hdisplay < hsync_start,
1974                    "hdisplay {} must be < hsync_start {}",
1975                    hdisplay,
1976                    hsync_start
1977                );
1978                ensure!(
1979                    hsync_start < hsync_end,
1980                    "hsync_start {} must be < hsync_end {}",
1981                    hsync_start,
1982                    hsync_end
1983                );
1984                ensure!(
1985                    hsync_end < htotal,
1986                    "hsync_end {} must be < htotal {}",
1987                    hsync_end,
1988                    htotal
1989                );
1990                ensure!(0 < *htotal, "htotal {} must be > 0", htotal);
1991                ensure!(
1992                    vdisplay < vsync_start,
1993                    "vdisplay {} must be < vsync_start {}",
1994                    vdisplay,
1995                    vsync_start
1996                );
1997                ensure!(
1998                    vsync_start < vsync_end,
1999                    "vsync_start {} must be < vsync_end {}",
2000                    vsync_start,
2001                    vsync_end
2002                );
2003                ensure!(
2004                    vsync_end < vtotal,
2005                    "vsync_end {} must be < vtotal {}",
2006                    vsync_end,
2007                    vtotal
2008                );
2009                ensure!(0 < *vtotal, "vtotal {} must be > 0", vtotal);
2010                Ok(())
2011            }
2012            OutputAction::CustomMode {
2013                mode: ConfiguredMode { refresh, .. },
2014            } => {
2015                if refresh.is_none() {
2016                    return Err("refresh rate is required for custom modes".to_string());
2017                }
2018                if let Some(refresh) = refresh {
2019                    if *refresh <= 0. {
2020                        return Err(format!("custom mode refresh rate {refresh} must be > 0"));
2021                    }
2022                }
2023                Ok(())
2024            }
2025            _ => Ok(()),
2026        }
2027    }
2028}
2029
2030#[cfg(test)]
2031mod tests {
2032    use super::*;
2033
2034    #[test]
2035    fn parse_size_change() {
2036        assert_eq!(
2037            "10".parse::<SizeChange>().unwrap(),
2038            SizeChange::SetFixed(10),
2039        );
2040        assert_eq!(
2041            "+10".parse::<SizeChange>().unwrap(),
2042            SizeChange::AdjustFixed(10),
2043        );
2044        assert_eq!(
2045            "-10".parse::<SizeChange>().unwrap(),
2046            SizeChange::AdjustFixed(-10),
2047        );
2048        assert_eq!(
2049            "10%".parse::<SizeChange>().unwrap(),
2050            SizeChange::SetProportion(10.),
2051        );
2052        assert_eq!(
2053            "+10%".parse::<SizeChange>().unwrap(),
2054            SizeChange::AdjustProportion(10.),
2055        );
2056        assert_eq!(
2057            "-10%".parse::<SizeChange>().unwrap(),
2058            SizeChange::AdjustProportion(-10.),
2059        );
2060
2061        assert!("-".parse::<SizeChange>().is_err());
2062        assert!("10% ".parse::<SizeChange>().is_err());
2063    }
2064
2065    #[test]
2066    fn parse_position_change() {
2067        assert_eq!(
2068            "10".parse::<PositionChange>().unwrap(),
2069            PositionChange::SetFixed(10.),
2070        );
2071        assert_eq!(
2072            "+10".parse::<PositionChange>().unwrap(),
2073            PositionChange::AdjustFixed(10.),
2074        );
2075        assert_eq!(
2076            "-10".parse::<PositionChange>().unwrap(),
2077            PositionChange::AdjustFixed(-10.),
2078        );
2079
2080        assert_eq!(
2081            "10%".parse::<PositionChange>().unwrap(),
2082            PositionChange::SetProportion(10.)
2083        );
2084        assert_eq!(
2085            "+10%".parse::<PositionChange>().unwrap(),
2086            PositionChange::AdjustProportion(10.)
2087        );
2088        assert_eq!(
2089            "-10%".parse::<PositionChange>().unwrap(),
2090            PositionChange::AdjustProportion(-10.)
2091        );
2092        assert!("-".parse::<PositionChange>().is_err());
2093        assert!("10% ".parse::<PositionChange>().is_err());
2094    }
2095}