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}