niri_ipc/
lib.rs

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