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