From b30e8e4b2e5689def12f9146a7d76efdbfee4361 Mon Sep 17 00:00:00 2001 From: faetalize Date: Tue, 28 Apr 2026 12:57:03 +0100 Subject: [PATCH 1/8] Add screenshot notification actions --- docs/wiki/Configuration:-Miscellaneous.md | 13 +++ niri-config/src/lib.rs | 26 +++++ niri-config/src/misc.rs | 25 +++++ resources/default-config.kdl | 7 ++ src/niri.rs | 7 +- src/utils/mod.rs | 128 +++++++++++++++++++++- 6 files changed, 200 insertions(+), 6 deletions(-) diff --git a/docs/wiki/Configuration:-Miscellaneous.md b/docs/wiki/Configuration:-Miscellaneous.md index 7a2f42dd26..2e21ecf8e1 100644 --- a/docs/wiki/Configuration:-Miscellaneous.md +++ b/docs/wiki/Configuration:-Miscellaneous.md @@ -135,6 +135,19 @@ You can also set this option to `null` to disable saving screenshots to disk. screenshot-path null ``` +### `screenshot-notification` + +Add action buttons to screenshot notifications. Actions are shown only for screenshots that were saved to disk. + +The `{path}` placeholder in command arguments is replaced with the saved screenshot path. + +```kdl +screenshot-notification { + action "Open" "xdg-open" "{path}" + action "Edit" "swappy" "-f" "{path}" +} +``` + ### `environment` Override environment variables for processes spawned by niri. diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 329b76696d..7ef9c4fa89 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -76,6 +76,7 @@ pub struct Config { pub prefer_no_csd: bool, pub cursor: Cursor, pub screenshot_path: ScreenshotPath, + pub screenshot_notification: ScreenshotNotification, pub clipboard: Clipboard, pub hotkey_overlay: HotkeyOverlay, pub config_notification: ConfigNotification, @@ -194,6 +195,7 @@ where "input" => m_merge!(input), "cursor" => m_merge!(cursor), "clipboard" => m_merge!(clipboard), + "screenshot-notification" => m_merge!(screenshot_notification), "hotkey-overlay" => m_merge!(hotkey_overlay), "config-notification" => m_merge!(config_notification), "animations" => m_merge!(animations), @@ -833,6 +835,11 @@ mod tests { screenshot-path "~/Screenshots/screenshot.png" + screenshot-notification { + action "Open" "xdg-open" "{path}" + action "Edit" "swappy" "-f" "{path}" + } + clipboard { disable-primary } @@ -1486,6 +1493,25 @@ mod tests { "~/Screenshots/screenshot.png", ), ), + screenshot_notification: ScreenshotNotification { + actions: [ + ScreenshotNotificationAction { + label: "Open", + command: [ + "xdg-open", + "{path}", + ], + }, + ScreenshotNotificationAction { + label: "Edit", + command: [ + "swappy", + "-f", + "{path}", + ], + }, + ], + }, clipboard: Clipboard { disable_primary: true, }, diff --git a/niri-config/src/misc.rs b/niri-config/src/misc.rs index 66a200ca35..3bea1f9940 100644 --- a/niri-config/src/misc.rs +++ b/niri-config/src/misc.rs @@ -64,6 +64,31 @@ impl Default for ScreenshotPath { } } +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct ScreenshotNotification { + pub actions: Vec, +} + +#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)] +pub struct ScreenshotNotificationPart { + #[knuffel(children(name = "action"))] + pub actions: Vec, +} + +impl MergeWith for ScreenshotNotification { + fn merge_with(&mut self, part: &ScreenshotNotificationPart) { + self.actions.clone_from(&part.actions); + } +} + +#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] +pub struct ScreenshotNotificationAction { + #[knuffel(argument)] + pub label: String, + #[knuffel(arguments)] + pub command: Vec, +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct HotkeyOverlay { pub skip_at_startup: bool, diff --git a/resources/default-config.kdl b/resources/default-config.kdl index ccad1ac22e..eb99728880 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -293,6 +293,13 @@ screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" // You can also set this to null to disable saving screenshots to disk. // screenshot-path null +// Add buttons to screenshot notifications. The buttons only appear for screenshots saved to disk. +// The {path} placeholder is replaced with the saved screenshot path. +// screenshot-notification { +// action "Open" "xdg-open" "{path}" +// action "Edit" "swappy" "-f" "{path}" +// } + // Animation settings. // The wiki explains how to configure individual animations: // https://niri-wm.github.io/niri/Configuration:-Animations diff --git a/src/niri.rs b/src/niri.rs index ec40e01c14..8fc39d3a7f 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -5690,6 +5690,8 @@ impl Niri { }) .unwrap(); + let notification_actions = self.config.borrow().screenshot_notification.actions.clone(); + // Encode and save the image in a thread as it's slow. thread::spawn(move || { let mut buf = vec![]; @@ -5732,7 +5734,10 @@ impl Niri { } #[cfg(feature = "dbus")] - if let Err(err) = crate::utils::show_screenshot_notification(image_path.as_deref()) { + if let Err(err) = crate::utils::show_screenshot_notification( + image_path.as_deref(), + ¬ification_actions, + ) { warn!("error showing screenshot notification: {err:?}"); } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index bbeac5bc6a..567074a44a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -13,7 +13,7 @@ use anyhow::{ensure, Context}; use bitflags::bitflags; use directories::UserDirs; use git_version::git_version; -use niri_config::{Config, OutputName}; +use niri_config::{Config, OutputName, ScreenshotNotificationAction}; use smithay::backend::renderer::utils::{ with_renderer_surface_state, RendererSurfaceStateUserData, }; @@ -541,9 +541,13 @@ pub fn baba_is_float_offset(now: Duration, view_height: f64) -> f64 { } #[cfg(feature = "dbus")] -pub fn show_screenshot_notification(image_path: Option<&Path>) -> anyhow::Result<()> { +pub fn show_screenshot_notification( + image_path: Option<&Path>, + actions: &[ScreenshotNotificationAction], +) -> anyhow::Result<()> { use std::collections::HashMap; + use anyhow::Context as _; use pango::glib; use zbus::zvariant; @@ -567,9 +571,24 @@ pub fn show_screenshot_notification(image_path: Option<&Path>) -> anyhow::Result } } - let actions: &[&str] = &[]; + let actions = if image_path.is_some() { + actions + .iter() + .filter(|action| !action.command.is_empty()) + .cloned() + .collect() + } else { + Vec::new() + }; - conn.call_method( + let mut action_strings = Vec::with_capacity(actions.len() * 2); + for (idx, action) in actions.iter().enumerate() { + action_strings.push(idx.to_string()); + action_strings.push(action.label.clone()); + } + let action_refs: Vec<_> = action_strings.iter().map(|s| s.as_str()).collect(); + + let reply = conn.call_method( Some("org.freedesktop.Notifications"), "/org/freedesktop/Notifications", Some("org.freedesktop.Notifications"), @@ -580,7 +599,7 @@ pub fn show_screenshot_notification(image_path: Option<&Path>) -> anyhow::Result image_url.as_ref().map(|url| url.as_str()).unwrap_or(""), "Screenshot captured", "You can paste the image from the clipboard.", - actions, + action_refs, HashMap::from([ ("transient", zvariant::Value::Bool(true)), ("urgency", zvariant::Value::U8(1)), @@ -589,9 +608,108 @@ pub fn show_screenshot_notification(image_path: Option<&Path>) -> anyhow::Result ), )?; + let notification_id: u32 = reply + .body() + .deserialize() + .context("error parsing notification id")?; + + if !actions.is_empty() { + if let Some(image_path) = image_path { + let image_path = image_path.to_owned(); + let conn = conn.inner().clone(); + let _ = std::thread::Builder::new() + .name("Screenshot Notification Actions".to_owned()) + .spawn(move || { + async_io::block_on(async move { + if let Err(err) = handle_screenshot_notification_actions( + conn, + notification_id, + image_path, + actions, + ) + .await + { + warn!("error handling screenshot notification action: {err:?}"); + } + }); + }); + } + } + Ok(()) } +#[cfg(feature = "dbus")] +async fn handle_screenshot_notification_actions( + conn: zbus::Connection, + notification_id: u32, + image_path: PathBuf, + actions: Vec, +) -> anyhow::Result<()> { + use anyhow::Context as _; + use futures_util::{select_biased, FutureExt as _, StreamExt as _}; + + let proxy = zbus::Proxy::new( + &conn, + "org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + ) + .await + .context("error creating notifications proxy")?; + + let mut action_invoked = proxy + .receive_signal("ActionInvoked") + .await + .context("error receiving ActionInvoked signal")?; + let mut notification_closed = proxy + .receive_signal("NotificationClosed") + .await + .context("error receiving NotificationClosed signal")?; + + loop { + select_biased! { + message = action_invoked.next().fuse() => { + let Some(message) = message else { + return Ok(()); + }; + let (id, action_key): (u32, String) = message + .body() + .deserialize() + .context("error parsing ActionInvoked signal")?; + if id != notification_id { + continue; + } + + if let Ok(idx) = action_key.parse::() { + if let Some(action) = actions.get(idx) { + let path = image_path.to_string_lossy(); + let command = action + .command + .iter() + .map(|arg| arg.replace("{path}", &path)) + .collect(); + spawning::spawn(command, None); + } + } + return Ok(()); + } + message = notification_closed.next().fuse() => { + let Some(message) = message else { + return Ok(()); + }; + let (id, _reason): (u32, u32) = message + .body() + .deserialize() + .context("error parsing NotificationClosed signal")?; + if id == notification_id { + return Ok(()); + } + } + } + } +} + #[inline(never)] pub fn cause_panic() { let a = Duration::from_secs(1); From 69eb5ab0ee0f93c99cc8b53c56d3a5fd04e72f99 Mon Sep 17 00:00:00 2001 From: faetalize Date: Tue, 28 Apr 2026 13:04:27 +0100 Subject: [PATCH 2/8] add Since annotation + address formatting issues --- docs/wiki/Configuration:-Miscellaneous.md | 2 ++ src/protocols/foreign_toplevel.rs | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/wiki/Configuration:-Miscellaneous.md b/docs/wiki/Configuration:-Miscellaneous.md index 2e21ecf8e1..23a6689732 100644 --- a/docs/wiki/Configuration:-Miscellaneous.md +++ b/docs/wiki/Configuration:-Miscellaneous.md @@ -137,6 +137,8 @@ screenshot-path null ### `screenshot-notification` +Since: 26.04 + Add action buttons to screenshot notifications. Actions are shown only for screenshots that were saved to disk. The `{path}` placeholder in command arguments is replaced with the saved screenshot path. diff --git a/src/protocols/foreign_toplevel.rs b/src/protocols/foreign_toplevel.rs index 11379169de..a8eb248d75 100644 --- a/src/protocols/foreign_toplevel.rs +++ b/src/protocols/foreign_toplevel.rs @@ -5,11 +5,13 @@ use std::sync::Arc; use arrayvec::ArrayVec; use smithay::output::Output; use smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::{ - ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1}, ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1}, + ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1}, + ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1}, }; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::{ - zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1}, zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, + zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1}, + zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, }; use smithay::reexports::wayland_server::backend::ClientId; use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; @@ -18,12 +20,12 @@ use smithay::reexports::wayland_server::{ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, }; use smithay::wayland::shell::xdg::{ - ToplevelState, ToplevelStateSet, XdgToplevelSurfaceRoleAttributes + ToplevelState, ToplevelStateSet, XdgToplevelSurfaceRoleAttributes, }; use crate::niri::State; -use crate::window::mapped::MappedId; use crate::utils::with_toplevel_role_and_current; +use crate::window::mapped::MappedId; const EXT_LIST_VERSION: u32 = 1; const WLR_MANAGEMENT_VERSION: u32 = 3; From 0b919960186925662b9aefeb30689ccf1e53dbe0 Mon Sep 17 00:00:00 2001 From: faetalize Date: Tue, 28 Apr 2026 16:11:45 +0100 Subject: [PATCH 3/8] screenshot: Move path and notification options into screenshot {} --- docs/wiki/Configuration:-Key-Bindings.md | 2 +- docs/wiki/Configuration:-Miscellaneous.md | 51 ++++++++++++--- docs/wiki/Development:-Design-Principles.md | 2 +- niri-config/src/lib.rs | 69 ++++++++++++--------- niri-config/src/misc.rs | 34 +++++++++- niri-ipc/src/lib.rs | 10 +-- resources/default-config.kdl | 31 ++++----- src/niri.rs | 2 +- src/utils/mod.rs | 2 +- 9 files changed, 139 insertions(+), 64 deletions(-) diff --git a/docs/wiki/Configuration:-Key-Bindings.md b/docs/wiki/Configuration:-Key-Bindings.md index f60df35bb9..5690149585 100644 --- a/docs/wiki/Configuration:-Key-Bindings.md +++ b/docs/wiki/Configuration:-Key-Bindings.md @@ -356,7 +356,7 @@ Actions for taking screenshots. - `screenshot`: opens the built-in interactive screenshot UI. - `screenshot-screen`, `screenshot-window`: takes a screenshot of the focused screen or window respectively. -The screenshot is both stored to the clipboard and saved to disk, according to the [`screenshot-path` option](./Configuration:-Miscellaneous.md#screenshot-path). +The screenshot is both stored to the clipboard and saved to disk, according to the [`screenshot.path` option](./Configuration:-Miscellaneous.md#path). Since: 25.02 You can disable saving to disk for a specific bind with the `write-to-disk=false` property: diff --git a/docs/wiki/Configuration:-Miscellaneous.md b/docs/wiki/Configuration:-Miscellaneous.md index 23a6689732..3a3cc2b8b4 100644 --- a/docs/wiki/Configuration:-Miscellaneous.md +++ b/docs/wiki/Configuration:-Miscellaneous.md @@ -9,7 +9,13 @@ spawn-sh-at-startup "qs -c ~/source/qs/MyAwesomeShell" prefer-no-csd -screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" +screenshot { + path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" + notification { + action "Open" "xdg-open" "{path}" + action "Edit" "swappy" "-f" "{path}" + } +} environment { QT_QPA_PLATFORM "wayland" @@ -116,7 +122,13 @@ With `prefer-no-csd` set, applications that negotiate server-side decorations th prefer-no-csd ``` -### `screenshot-path` +### `screenshot` + +Since: next release + +Settings for screenshots taken with the built-in niri screenshot tool. + +#### `path` Set the path where screenshots are saved. A `~` at the front will be expanded to the home directory. @@ -126,30 +138,49 @@ The path is formatted with `strftime(3)` to give you the screenshot date and tim Niri will create the last folder of the path if it doesn't exist. ```kdl -screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" +screenshot { + path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" +} ``` You can also set this option to `null` to disable saving screenshots to disk. ```kdl -screenshot-path null +screenshot { + path null +} ``` -### `screenshot-notification` +Until: 26.04 For backwards compatibility, the old top-level `screenshot-path` option is still supported. It is used if `screenshot.path` is not set. -Since: 26.04 +```kdl +// Deprecated +screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" +``` + + +#### `notification` -Add action buttons to screenshot notifications. Actions are shown only for screenshots that were saved to disk. +Since: next release + +Add action buttons to the screenshot notification. Actions are shown only for screenshots that were saved to disk. The `{path}` placeholder in command arguments is replaced with the saved screenshot path. ```kdl -screenshot-notification { - action "Open" "xdg-open" "{path}" - action "Edit" "swappy" "-f" "{path}" +screenshot { + notification { + action "Open" "xdg-open" "{path}" + action "Edit" "swappy" "-f" "{path}" + } } ``` +The notification block accepts any number of `action` entries, which have the following format: + +`action `. + + ### `environment` Override environment variables for processes spawned by niri. diff --git a/docs/wiki/Development:-Design-Principles.md b/docs/wiki/Development:-Design-Principles.md index 7a3892a841..66b3a14059 100644 --- a/docs/wiki/Development:-Design-Principles.md +++ b/docs/wiki/Development:-Design-Principles.md @@ -98,7 +98,7 @@ The default input settings like touchpad tap and natural-scroll are how I believ Shadows default to off because they are a fairly performance-intensive shader, and because many clientside-decorated windows already draw their own shadows. -The default screenshot-path matches GNOME Shell. +The default screenshot path matches GNOME Shell. Default window rules are limited to fixing known severe issues (WezTerm) and doing something the absolute majority likely wants (make Firefox Picture-in-Picture player floating—it can't do that on its own currently, maybe the pip protocol will change that). diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 7ef9c4fa89..b79ce697c0 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -75,8 +75,7 @@ pub struct Config { pub layout: Layout, pub prefer_no_csd: bool, pub cursor: Cursor, - pub screenshot_path: ScreenshotPath, - pub screenshot_notification: ScreenshotNotification, + pub screenshot: Screenshot, pub clipboard: Clipboard, pub hotkey_overlay: HotkeyOverlay, pub config_notification: ConfigNotification, @@ -194,8 +193,8 @@ where match name { "input" => m_merge!(input), "cursor" => m_merge!(cursor), + "screenshot" => m_merge!(screenshot), "clipboard" => m_merge!(clipboard), - "screenshot-notification" => m_merge!(screenshot_notification), "hotkey-overlay" => m_merge!(hotkey_overlay), "config-notification" => m_merge!(config_notification), "animations" => m_merge!(animations), @@ -242,7 +241,7 @@ where "screenshot-path" => { let part = knuffel::Decode::decode_node(node, ctx)?; - config.borrow_mut().screenshot_path = part; + config.borrow_mut().screenshot.path = part; } "layout" => { @@ -650,6 +649,12 @@ mod tests { assert_eq!(config.input.keyboard.repeat_rate, 25); } + #[test] + fn legacy_screenshot_path() { + let config = Config::parse_mem(r#"screenshot-path "~/foo.png""#).unwrap(); + assert_eq!(config.screenshot.path.0.as_deref(), Some("~/foo.png")); + } + #[track_caller] fn do_parse(text: &str) -> Config { Config::parse_mem(text) @@ -833,11 +838,13 @@ mod tests { hide-after-inactive-ms 3000 } - screenshot-path "~/Screenshots/screenshot.png" + screenshot { + path "~/Screenshots/screenshot.png" - screenshot-notification { - action "Open" "xdg-open" "{path}" - action "Edit" "swappy" "-f" "{path}" + notification { + action "Open" "xdg-open" "{path}" + action "Edit" "swappy" "-f" "{path}" + } } clipboard { @@ -1488,29 +1495,31 @@ mod tests { 3000, ), }, - screenshot_path: ScreenshotPath( - Some( - "~/Screenshots/screenshot.png", + screenshot: Screenshot { + path: ScreenshotPath( + Some( + "~/Screenshots/screenshot.png", + ), ), - ), - screenshot_notification: ScreenshotNotification { - actions: [ - ScreenshotNotificationAction { - label: "Open", - command: [ - "xdg-open", - "{path}", - ], - }, - ScreenshotNotificationAction { - label: "Edit", - command: [ - "swappy", - "-f", - "{path}", - ], - }, - ], + notification: ScreenshotNotification { + actions: [ + ScreenshotNotificationAction { + label: "Open", + command: [ + "xdg-open", + "{path}", + ], + }, + ScreenshotNotificationAction { + label: "Edit", + command: [ + "swappy", + "-f", + "{path}", + ], + }, + ], + }, }, clipboard: Clipboard { disable_primary: true, diff --git a/niri-config/src/misc.rs b/niri-config/src/misc.rs index 3bea1f9940..d3c3715721 100644 --- a/niri-config/src/misc.rs +++ b/niri-config/src/misc.rs @@ -53,7 +53,7 @@ impl MergeWith for Cursor { } } -#[derive(knuffel::Decode, Debug, Clone, PartialEq)] +#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] pub struct ScreenshotPath(#[knuffel(argument)] pub Option); impl Default for ScreenshotPath { @@ -64,6 +64,38 @@ impl Default for ScreenshotPath { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Screenshot { + pub path: ScreenshotPath, + pub notification: ScreenshotNotification, +} + +impl Default for Screenshot { + fn default() -> Self { + Self { + path: ScreenshotPath::default(), + notification: ScreenshotNotification::default(), + } + } +} + +#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)] +pub struct ScreenshotPart { + #[knuffel(child)] + pub path: Option, + #[knuffel(child)] + pub notification: Option, +} + +impl MergeWith for Screenshot { + fn merge_with(&mut self, part: &ScreenshotPart) { + merge_clone!((self, part), path); + if let Some(notification) = &part.notification { + self.notification.merge_with(notification); + } + } +} + #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct ScreenshotNotification { pub actions: Vec, diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index 0aa3cb4f48..93a4fccb73 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -230,7 +230,7 @@ pub enum Action { /// /// The path must be absolute, otherwise an error is returned. /// - /// If `None`, the screenshot is saved according to the `screenshot-path` config setting. + /// If `None`, the screenshot is saved according to the `screenshot.path` config setting. #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))] path: Option, }, @@ -238,7 +238,7 @@ pub enum Action { ScreenshotScreen { /// Write the screenshot to disk in addition to putting it in your clipboard. /// - /// The screenshot is saved according to the `screenshot-path` config setting. + /// The screenshot is saved according to the `screenshot.path` config setting. #[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))] write_to_disk: bool, @@ -250,7 +250,7 @@ pub enum Action { /// /// The path must be absolute, otherwise an error is returned. /// - /// If `None`, the screenshot is saved according to the `screenshot-path` config setting. + /// If `None`, the screenshot is saved according to the `screenshot.path` config setting. #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))] path: Option, }, @@ -264,7 +264,7 @@ pub enum Action { id: Option, /// Write the screenshot to disk in addition to putting it in your clipboard. /// - /// The screenshot is saved according to the `screenshot-path` config setting. + /// The screenshot is saved according to the `screenshot.path` config setting. #[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))] write_to_disk: bool, @@ -279,7 +279,7 @@ pub enum Action { /// /// The path must be absolute, otherwise an error is returned. /// - /// If `None`, the screenshot is saved according to the `screenshot-path` config setting. + /// If `None`, the screenshot is saved according to the `screenshot.path` config setting. #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))] path: Option, }, diff --git a/resources/default-config.kdl b/resources/default-config.kdl index eb99728880..064a2b505d 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -285,20 +285,23 @@ hotkey-overlay { // After enabling or disabling this, you need to restart the apps for this to take effect. // prefer-no-csd -// You can change the path where screenshots are saved. -// A ~ at the front will be expanded to the home directory. -// The path is formatted with strftime(3) to give you the screenshot date and time. -screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" - -// You can also set this to null to disable saving screenshots to disk. -// screenshot-path null - -// Add buttons to screenshot notifications. The buttons only appear for screenshots saved to disk. -// The {path} placeholder is replaced with the saved screenshot path. -// screenshot-notification { -// action "Open" "xdg-open" "{path}" -// action "Edit" "swappy" "-f" "{path}" -// } +// Screenshot settings. +screenshot { + // You can change the path where screenshots are saved. + // A ~ at the front will be expanded to the home directory. + // The path is formatted with strftime(3) to give you the screenshot date and time. + path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" + + // You can also set this to null to disable saving screenshots to disk. + // path null + + // Add buttons to screenshot notifications. The buttons only appear for screenshots saved to disk. + // The {path} placeholder is replaced with the saved screenshot path. + // notification { + // action "Open" "xdg-open" "{path}" + // action "Edit" "swappy" "-f" "{path}" + // } +} // Animation settings. // The wiki explains how to configure individual animations: diff --git a/src/niri.rs b/src/niri.rs index 8fc39d3a7f..77b5996ae7 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -5690,7 +5690,7 @@ impl Niri { }) .unwrap(); - let notification_actions = self.config.borrow().screenshot_notification.actions.clone(); + let notification_actions = self.config.borrow().screenshot.notification.actions.clone(); // Encode and save the image in a thread as it's slow. thread::spawn(move || { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 567074a44a..f5f5abd414 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -277,7 +277,7 @@ pub fn expand_home(path: &Path) -> anyhow::Result> { } pub fn make_screenshot_path(config: &Config) -> anyhow::Result> { - let Some(path) = &config.screenshot_path.0 else { + let Some(path) = &config.screenshot.path.0 else { return Ok(None); }; From e389f2fcd8fd5ddb417a02e5f69b099c8e1f2596 Mon Sep 17 00:00:00 2001 From: faetalize Date: Tue, 28 Apr 2026 16:14:56 +0100 Subject: [PATCH 4/8] docs: Update screenshot path references in configuration files --- docs/wiki/Configuration:-Key-Bindings.md | 2 +- docs/wiki/Configuration:-Miscellaneous.md | 2 +- niri-ipc/src/lib.rs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/wiki/Configuration:-Key-Bindings.md b/docs/wiki/Configuration:-Key-Bindings.md index 5690149585..12e83fa81e 100644 --- a/docs/wiki/Configuration:-Key-Bindings.md +++ b/docs/wiki/Configuration:-Key-Bindings.md @@ -356,7 +356,7 @@ Actions for taking screenshots. - `screenshot`: opens the built-in interactive screenshot UI. - `screenshot-screen`, `screenshot-window`: takes a screenshot of the focused screen or window respectively. -The screenshot is both stored to the clipboard and saved to disk, according to the [`screenshot.path` option](./Configuration:-Miscellaneous.md#path). +The screenshot is both stored to the clipboard and saved to disk, according to the [`screenshot { path ""; }` option](./Configuration:-Miscellaneous.md#path). Since: 25.02 You can disable saving to disk for a specific bind with the `write-to-disk=false` property: diff --git a/docs/wiki/Configuration:-Miscellaneous.md b/docs/wiki/Configuration:-Miscellaneous.md index 3a3cc2b8b4..d0d7018a38 100644 --- a/docs/wiki/Configuration:-Miscellaneous.md +++ b/docs/wiki/Configuration:-Miscellaneous.md @@ -151,7 +151,7 @@ screenshot { } ``` -Until: 26.04 For backwards compatibility, the old top-level `screenshot-path` option is still supported. It is used if `screenshot.path` is not set. +Until: 26.04 For backwards compatibility, the old top-level `screenshot-path` option is still supported. It is used if `screenshot { path ""; }` is not set. ```kdl // Deprecated diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index 93a4fccb73..934abd676a 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -230,7 +230,7 @@ pub enum Action { /// /// The path must be absolute, otherwise an error is returned. /// - /// If `None`, the screenshot is saved according to the `screenshot.path` config setting. + /// If `None`, the screenshot is saved according to the `screenshot { path ""; }` config setting. #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))] path: Option, }, @@ -238,7 +238,7 @@ pub enum Action { ScreenshotScreen { /// Write the screenshot to disk in addition to putting it in your clipboard. /// - /// The screenshot is saved according to the `screenshot.path` config setting. + /// The screenshot is saved according to the `screenshot { path ""; }` config setting. #[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))] write_to_disk: bool, @@ -250,7 +250,7 @@ pub enum Action { /// /// The path must be absolute, otherwise an error is returned. /// - /// If `None`, the screenshot is saved according to the `screenshot.path` config setting. + /// If `None`, the screenshot is saved according to the `screenshot { path ""; }` config setting. #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))] path: Option, }, @@ -264,7 +264,7 @@ pub enum Action { id: Option, /// Write the screenshot to disk in addition to putting it in your clipboard. /// - /// The screenshot is saved according to the `screenshot.path` config setting. + /// The screenshot is saved according to the `screenshot { path ""; }` config setting. #[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))] write_to_disk: bool, @@ -279,7 +279,7 @@ pub enum Action { /// /// The path must be absolute, otherwise an error is returned. /// - /// If `None`, the screenshot is saved according to the `screenshot.path` config setting. + /// If `None`, the screenshot is saved according to the `screenshot { path ""; }` config setting. #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))] path: Option, }, From c4392ec40756b4fb658d1cca8b19f6910399e122 Mon Sep 17 00:00:00 2001 From: faetalize Date: Tue, 28 Apr 2026 16:19:57 +0100 Subject: [PATCH 5/8] refactor: remove unnecessary use of anyhow::Context in screenshot notification functions --- src/utils/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f5f5abd414..deacceb053 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -547,7 +547,6 @@ pub fn show_screenshot_notification( ) -> anyhow::Result<()> { use std::collections::HashMap; - use anyhow::Context as _; use pango::glib; use zbus::zvariant; @@ -646,7 +645,6 @@ async fn handle_screenshot_notification_actions( image_path: PathBuf, actions: Vec, ) -> anyhow::Result<()> { - use anyhow::Context as _; use futures_util::{select_biased, FutureExt as _, StreamExt as _}; let proxy = zbus::Proxy::new( From f0e6eb323c3d20ca7a7b89a724fad13bdada7045 Mon Sep 17 00:00:00 2001 From: faetalize Date: Tue, 28 Apr 2026 17:51:02 +0100 Subject: [PATCH 6/8] fix: handle screenshot notification actions path replacement correctly (exclusively {path}) --- src/utils/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index deacceb053..0f0e77731c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -685,7 +685,13 @@ async fn handle_screenshot_notification_actions( let command = action .command .iter() - .map(|arg| arg.replace("{path}", &path)) + .map(|arg| { + if arg == "{path}" { + path.to_string() + } else { + arg.clone() + } + }) .collect(); spawning::spawn(command, None); } From 64172ea0a750449d00abd3d1495638601a12ff60 Mon Sep 17 00:00:00 2001 From: faetalize Date: Tue, 28 Apr 2026 17:55:16 +0100 Subject: [PATCH 7/8] refactor: optimize action string handling in show_screenshot_notification function --- src/utils/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0f0e77731c..744c469205 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,5 @@ use std::cmp::{max, min}; +use std::borrow::Cow; use std::ffi::{CString, OsStr}; use std::fmt::Display; use std::io::Write; @@ -580,12 +581,11 @@ pub fn show_screenshot_notification( Vec::new() }; - let mut action_strings = Vec::with_capacity(actions.len() * 2); - for (idx, action) in actions.iter().enumerate() { - action_strings.push(idx.to_string()); - action_strings.push(action.label.clone()); - } - let action_refs: Vec<_> = action_strings.iter().map(|s| s.as_str()).collect(); + let action_strings: Vec> = actions + .iter() + .enumerate() + .flat_map(|(idx, action)| [idx.to_string().into(), action.label.as_str().into()]) + .collect(); let reply = conn.call_method( Some("org.freedesktop.Notifications"), @@ -598,7 +598,7 @@ pub fn show_screenshot_notification( image_url.as_ref().map(|url| url.as_str()).unwrap_or(""), "Screenshot captured", "You can paste the image from the clipboard.", - action_refs, + action_strings, HashMap::from([ ("transient", zvariant::Value::Bool(true)), ("urgency", zvariant::Value::U8(1)), From e42187e5ff9fcc42cef4e8ad70708ea429574794 Mon Sep 17 00:00:00 2001 From: faetalize Date: Tue, 28 Apr 2026 18:00:03 +0100 Subject: [PATCH 8/8] fmt run --- niri-ipc/src/lib.rs | 9 ++++++--- src/utils/mod.rs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index 934abd676a..73d6988f82 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -230,7 +230,8 @@ pub enum Action { /// /// The path must be absolute, otherwise an error is returned. /// - /// If `None`, the screenshot is saved according to the `screenshot { path ""; }` config setting. + /// If `None`, the screenshot is saved according to the `screenshot { path ""; }` config + /// setting. #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))] path: Option, }, @@ -250,7 +251,8 @@ pub enum Action { /// /// The path must be absolute, otherwise an error is returned. /// - /// If `None`, the screenshot is saved according to the `screenshot { path ""; }` config setting. + /// If `None`, the screenshot is saved according to the `screenshot { path ""; }` config + /// setting. #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))] path: Option, }, @@ -279,7 +281,8 @@ pub enum Action { /// /// The path must be absolute, otherwise an error is returned. /// - /// If `None`, the screenshot is saved according to the `screenshot { path ""; }` config setting. + /// If `None`, the screenshot is saved according to the `screenshot { path ""; }` config + /// setting. #[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set))] path: Option, }, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 744c469205..db2155a10a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,5 @@ -use std::cmp::{max, min}; use std::borrow::Cow; +use std::cmp::{max, min}; use std::ffi::{CString, OsStr}; use std::fmt::Display; use std::io::Write;