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