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