Skip to content
Open
2 changes: 1 addition & 1 deletion docs/wiki/Configuration:-Key-Bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<sup>Since: 25.02</sup> You can disable saving to disk for a specific bind with the `write-to-disk=false` property:

Expand Down
54 changes: 50 additions & 4 deletions docs/wiki/Configuration:-Miscellaneous.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -116,7 +122,13 @@ With `prefer-no-csd` set, applications that negotiate server-side decorations th
prefer-no-csd
```

### `screenshot-path`
### `screenshot`

<sup>Since: next release</sup>

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.
Expand All @@ -126,15 +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
}
```

<sup>Until: 26.04</sup> For backwards compatibility, the old top-level `screenshot-path` option is still supported. It is used if `screenshot { path ""; }` is not set.

```kdl
// Deprecated
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
```


#### `notification`

<sup>Since: next release</sup>

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}"
}
}
```

The notification block accepts any number of `action` entries, which have the following format:

`action <name> <command...>`.


### `environment`

Override environment variables for processes spawned by niri.
Expand Down
2 changes: 1 addition & 1 deletion docs/wiki/Development:-Design-Principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
49 changes: 42 additions & 7 deletions niri-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub struct Config {
pub layout: Layout,
pub prefer_no_csd: bool,
pub cursor: Cursor,
pub screenshot_path: ScreenshotPath,
pub screenshot: Screenshot,
pub clipboard: Clipboard,
pub hotkey_overlay: HotkeyOverlay,
pub config_notification: ConfigNotification,
Expand Down Expand Up @@ -193,6 +193,7 @@ where
match name {
"input" => m_merge!(input),
"cursor" => m_merge!(cursor),
"screenshot" => m_merge!(screenshot),
"clipboard" => m_merge!(clipboard),
"hotkey-overlay" => m_merge!(hotkey_overlay),
"config-notification" => m_merge!(config_notification),
Expand Down Expand Up @@ -240,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" => {
Expand Down Expand Up @@ -648,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)
Expand Down Expand Up @@ -832,7 +839,14 @@ mod tests {
hide-after-inactive-ms 3000
}

screenshot-path "~/Screenshots/screenshot.png"
screenshot {
path "~/Screenshots/screenshot.png"

notification {
action "Open" "xdg-open" "{path}"
action "Edit" "swappy" "-f" "{path}"
}
}

clipboard {
disable-primary
Expand Down Expand Up @@ -1483,11 +1497,32 @@ mod tests {
3000,
),
},
screenshot_path: ScreenshotPath(
Some(
"~/Screenshots/screenshot.png",
screenshot: Screenshot {
path: ScreenshotPath(
Some(
"~/Screenshots/screenshot.png",
),
),
),
notification: ScreenshotNotification {
actions: [
ScreenshotNotificationAction {
label: "Open",
command: [
"xdg-open",
"{path}",
],
},
ScreenshotNotificationAction {
label: "Edit",
command: [
"swappy",
"-f",
"{path}",
],
},
],
},
},
clipboard: Clipboard {
disable_primary: true,
},
Expand Down
59 changes: 58 additions & 1 deletion niri-config/src/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl MergeWith<CursorPart> for Cursor {
}
}

#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct ScreenshotPath(#[knuffel(argument)] pub Option<String>);

impl Default for ScreenshotPath {
Expand All @@ -64,6 +64,63 @@ 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<ScreenshotPath>,
#[knuffel(child)]
pub notification: Option<ScreenshotNotificationPart>,
}

impl MergeWith<ScreenshotPart> 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<ScreenshotNotificationAction>,
}

#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)]
pub struct ScreenshotNotificationPart {
#[knuffel(children(name = "action"))]
pub actions: Vec<ScreenshotNotificationAction>,
}

impl MergeWith<ScreenshotNotificationPart> 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<String>,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct HotkeyOverlay {
pub skip_at_startup: bool,
Expand Down
13 changes: 8 additions & 5 deletions niri-ipc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,16 @@ 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<String>,
},
/// Screenshot the focused screen.
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,

Expand All @@ -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<String>,
},
Expand All @@ -264,7 +266,7 @@ pub enum Action {
id: Option<u64>,
/// 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,

Expand All @@ -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<String>,
},
Expand Down
24 changes: 17 additions & 7 deletions resources/default-config.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +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
// 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:
Expand Down
7 changes: 6 additions & 1 deletion src/niri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![];
Expand Down Expand Up @@ -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(),
&notification_actions,
) {
warn!("error showing screenshot notification: {err:?}");
}

Expand Down
10 changes: 6 additions & 4 deletions src/protocols/foreign_toplevel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Comment thread
faetalize marked this conversation as resolved.
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;
Expand Down
Loading