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