Skip to main content

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