mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Add spawn-sh, spawn-at-startup-sh
Our top 10 most confusing config moments
This commit is contained in:
@@ -206,7 +206,8 @@ binds {
|
|||||||
> }
|
> }
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
Currently, niri *does not* use a shell to run commands, which means that you need to manually separate arguments.
|
For `spawn`, niri *does not* use a shell to run commands, which means that you need to manually separate arguments.
|
||||||
|
See [`spawn-sh`](#spawn-sh) below for an action that uses a shell.
|
||||||
|
|
||||||
```kdl
|
```kdl
|
||||||
binds {
|
binds {
|
||||||
@@ -249,6 +250,37 @@ binds {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `spawn-sh`
|
||||||
|
|
||||||
|
<sup>Since: next release</sup>
|
||||||
|
|
||||||
|
Run a command through the shell.
|
||||||
|
|
||||||
|
The argument is a single string that is passed verbatim to `sh`.
|
||||||
|
You can use shell variables, pipelines, `~` expansion, and everything else as expected.
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
binds {
|
||||||
|
// Works with spawn-sh: all arguments in the same string.
|
||||||
|
Mod+D { spawn-sh "alacritty -e /usr/bin/fish"; }
|
||||||
|
|
||||||
|
// Works with spawn-sh: shell variable ($MAIN_OUTPUT), ~ expansion.
|
||||||
|
Mod+T { spawn-sh "grim -o $MAIN_OUTPUT ~/screenshot.png"; }
|
||||||
|
|
||||||
|
// Works with spawn-sh: process substitution.
|
||||||
|
Mod+Q { spawn-sh "notify-send clipboard \"$(wl-paste)\""; }
|
||||||
|
|
||||||
|
// Works with spawn-sh: multiple commands.
|
||||||
|
Super+Alt+S { spawn-sh "pkill orca || exec orca"; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`spawn-sh "some command"` is equivalent to `spawn "sh" "-c" "some command"`—it's just a less confusing shorthand.
|
||||||
|
Keep in mind that going through the shell incurs a tiny performance penalty compared to directly `spawn`ing some binary.
|
||||||
|
|
||||||
|
Using `sh` is hardcoded, consistent with other compositors.
|
||||||
|
If you want a different shell, write it out using `spawn`, e.g. `spawn "fish" "-c" "some fish command"`.
|
||||||
|
|
||||||
#### `quit`
|
#### `quit`
|
||||||
|
|
||||||
Exit niri after showing a confirmation dialog to avoid accidentally triggering it.
|
Exit niri after showing a confirmation dialog to avoid accidentally triggering it.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Here are all of these options at a glance:
|
|||||||
```kdl
|
```kdl
|
||||||
spawn-at-startup "waybar"
|
spawn-at-startup "waybar"
|
||||||
spawn-at-startup "alacritty"
|
spawn-at-startup "alacritty"
|
||||||
|
spawn-at-startup-sh "qs -c ~/source/qs/MyAwesomeShell"
|
||||||
|
|
||||||
prefer-no-csd
|
prefer-no-csd
|
||||||
|
|
||||||
@@ -71,6 +72,22 @@ spawn-at-startup "alacritty"
|
|||||||
Note that running niri as a systemd session supports xdg-desktop-autostart out of the box, which may be more convenient to use.
|
Note that running niri as a systemd session supports xdg-desktop-autostart out of the box, which may be more convenient to use.
|
||||||
Thanks to this, apps that you configured to autostart in GNOME will also "just work" in niri, without any manual `spawn-at-startup` configuration.
|
Thanks to this, apps that you configured to autostart in GNOME will also "just work" in niri, without any manual `spawn-at-startup` configuration.
|
||||||
|
|
||||||
|
### `spawn-at-startup-sh`
|
||||||
|
|
||||||
|
<sup>Since: next release</sup>
|
||||||
|
|
||||||
|
Add lines like this to run shell commands at niri startup.
|
||||||
|
|
||||||
|
The argument is a single string that is passed verbatim to `sh`.
|
||||||
|
You can use shell variables, pipelines, `~` expansion and everything else as expected.
|
||||||
|
|
||||||
|
See detailed description in the docs for the [`spawn-sh` key binding action](./Configuration:-Key-Bindings.md#spawn-sh).
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
// Pass all arguments in the same string.
|
||||||
|
spawn-at-startup-sh "qs -c ~/source/qs/MyAwesomeShell"
|
||||||
|
```
|
||||||
|
|
||||||
### `prefer-no-csd`
|
### `prefer-no-csd`
|
||||||
|
|
||||||
This flag will make niri ask the applications to omit their client-side decorations.
|
This flag will make niri ask the applications to omit their client-side decorations.
|
||||||
|
|||||||
+36
-1
@@ -41,6 +41,8 @@ pub struct Config {
|
|||||||
pub outputs: Outputs,
|
pub outputs: Outputs,
|
||||||
#[knuffel(children(name = "spawn-at-startup"))]
|
#[knuffel(children(name = "spawn-at-startup"))]
|
||||||
pub spawn_at_startup: Vec<SpawnAtStartup>,
|
pub spawn_at_startup: Vec<SpawnAtStartup>,
|
||||||
|
#[knuffel(children(name = "spawn-at-startup-sh"))]
|
||||||
|
pub spawn_at_startup_sh: Vec<SpawnAtStartupSh>,
|
||||||
#[knuffel(child, default)]
|
#[knuffel(child, default)]
|
||||||
pub layout: Layout,
|
pub layout: Layout,
|
||||||
#[knuffel(child, default)]
|
#[knuffel(child, default)]
|
||||||
@@ -606,6 +608,12 @@ pub struct SpawnAtStartup {
|
|||||||
pub command: Vec<String>,
|
pub command: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SpawnAtStartupSh {
|
||||||
|
#[knuffel(argument)]
|
||||||
|
pub command: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct FocusRing {
|
pub struct FocusRing {
|
||||||
#[knuffel(child)]
|
#[knuffel(child)]
|
||||||
@@ -1719,6 +1727,7 @@ pub enum Action {
|
|||||||
DebugToggleOpaqueRegions,
|
DebugToggleOpaqueRegions,
|
||||||
DebugToggleDamage,
|
DebugToggleDamage,
|
||||||
Spawn(#[knuffel(arguments)] Vec<String>),
|
Spawn(#[knuffel(arguments)] Vec<String>),
|
||||||
|
SpawnSh(#[knuffel(argument)] String),
|
||||||
DoScreenTransition(#[knuffel(property(name = "delay-ms"))] Option<u16>),
|
DoScreenTransition(#[knuffel(property(name = "delay-ms"))] Option<u16>),
|
||||||
#[knuffel(skip)]
|
#[knuffel(skip)]
|
||||||
ConfirmScreenshot {
|
ConfirmScreenshot {
|
||||||
@@ -1962,6 +1971,7 @@ impl From<niri_ipc::Action> for Action {
|
|||||||
niri_ipc::Action::PowerOffMonitors {} => Self::PowerOffMonitors,
|
niri_ipc::Action::PowerOffMonitors {} => Self::PowerOffMonitors,
|
||||||
niri_ipc::Action::PowerOnMonitors {} => Self::PowerOnMonitors,
|
niri_ipc::Action::PowerOnMonitors {} => Self::PowerOnMonitors,
|
||||||
niri_ipc::Action::Spawn { command } => Self::Spawn(command),
|
niri_ipc::Action::Spawn { command } => Self::Spawn(command),
|
||||||
|
niri_ipc::Action::SpawnSh { command } => Self::SpawnSh(command),
|
||||||
niri_ipc::Action::DoScreenTransition { delay_ms } => Self::DoScreenTransition(delay_ms),
|
niri_ipc::Action::DoScreenTransition { delay_ms } => Self::DoScreenTransition(delay_ms),
|
||||||
niri_ipc::Action::Screenshot { show_pointer } => Self::Screenshot(show_pointer),
|
niri_ipc::Action::Screenshot { show_pointer } => Self::Screenshot(show_pointer),
|
||||||
niri_ipc::Action::ScreenshotScreen {
|
niri_ipc::Action::ScreenshotScreen {
|
||||||
@@ -3854,7 +3864,7 @@ where
|
|||||||
}
|
}
|
||||||
match Action::decode_node(child, ctx) {
|
match Action::decode_node(child, ctx) {
|
||||||
Ok(action) => {
|
Ok(action) => {
|
||||||
if !matches!(action, Action::Spawn(_)) {
|
if !matches!(action, Action::Spawn(_) | Action::SpawnSh(_)) {
|
||||||
if let Some(node) = allow_when_locked_node {
|
if let Some(node) = allow_when_locked_node {
|
||||||
ctx.emit_error(DecodeError::unexpected(
|
ctx.emit_error(DecodeError::unexpected(
|
||||||
node,
|
node,
|
||||||
@@ -4453,6 +4463,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
spawn-at-startup "alacritty" "-e" "fish"
|
spawn-at-startup "alacritty" "-e" "fish"
|
||||||
|
spawn-at-startup-sh "qs -c ~/source/qs/MyAwesomeShell"
|
||||||
|
|
||||||
prefer-no-csd
|
prefer-no-csd
|
||||||
|
|
||||||
@@ -4549,6 +4560,7 @@ mod tests {
|
|||||||
Mod+Shift+1 { focus-workspace "workspace-1"; }
|
Mod+Shift+1 { focus-workspace "workspace-1"; }
|
||||||
Mod+Shift+E allow-inhibiting=false { quit skip-confirmation=true; }
|
Mod+Shift+E allow-inhibiting=false { quit skip-confirmation=true; }
|
||||||
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||||
|
Super+Alt+S allow-when-locked=true { spawn-sh "pkill orca || exec orca"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
switch-events {
|
switch-events {
|
||||||
@@ -4794,6 +4806,11 @@ mod tests {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
spawn_at_startup_sh: [
|
||||||
|
SpawnAtStartupSh {
|
||||||
|
command: "qs -c ~/source/qs/MyAwesomeShell",
|
||||||
|
},
|
||||||
|
],
|
||||||
layout: Layout {
|
layout: Layout {
|
||||||
focus_ring: FocusRing {
|
focus_ring: FocusRing {
|
||||||
off: false,
|
off: false,
|
||||||
@@ -5681,6 +5698,24 @@ mod tests {
|
|||||||
allow_inhibiting: true,
|
allow_inhibiting: true,
|
||||||
hotkey_overlay_title: None,
|
hotkey_overlay_title: None,
|
||||||
},
|
},
|
||||||
|
Bind {
|
||||||
|
key: Key {
|
||||||
|
trigger: Keysym(
|
||||||
|
XK_s,
|
||||||
|
),
|
||||||
|
modifiers: Modifiers(
|
||||||
|
ALT | SUPER,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
action: SpawnSh(
|
||||||
|
"pkill orca || exec orca",
|
||||||
|
),
|
||||||
|
repeat: true,
|
||||||
|
cooldown: None,
|
||||||
|
allow_when_locked: true,
|
||||||
|
allow_inhibiting: true,
|
||||||
|
hotkey_overlay_title: None,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
switch_events: SwitchBinds {
|
switch_events: SwitchBinds {
|
||||||
|
|||||||
@@ -203,6 +203,12 @@ pub enum Action {
|
|||||||
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
|
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
|
||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
},
|
},
|
||||||
|
/// Spawn a command through the shell.
|
||||||
|
SpawnSh {
|
||||||
|
/// Command to run.
|
||||||
|
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
|
||||||
|
command: String,
|
||||||
|
},
|
||||||
/// Do a screen transition.
|
/// Do a screen transition.
|
||||||
DoScreenTransition {
|
DoScreenTransition {
|
||||||
/// Delay in milliseconds for the screen to freeze before starting the transition.
|
/// Delay in milliseconds for the screen to freeze before starting the transition.
|
||||||
|
|||||||
@@ -270,6 +270,9 @@ layout {
|
|||||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||||
spawn-at-startup "waybar"
|
spawn-at-startup "waybar"
|
||||||
|
|
||||||
|
// To run a shell command (with variables, pipes, etc.), use spawn-at-startup-sh:
|
||||||
|
// spawn-at-startup-sh "qs -c ~/source/qs/MyAwesomeShell"
|
||||||
|
|
||||||
hotkey-overlay {
|
hotkey-overlay {
|
||||||
// Uncomment this line to disable the "Important Hotkeys" pop-up at startup.
|
// Uncomment this line to disable the "Important Hotkeys" pop-up at startup.
|
||||||
// skip-at-startup
|
// skip-at-startup
|
||||||
@@ -363,10 +366,10 @@ binds {
|
|||||||
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
|
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
|
||||||
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
|
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
|
||||||
|
|
||||||
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
|
// Use spawn-sh to run a shell command. Do this if you need pipes, multiple commands, etc.
|
||||||
// Note: the entire command goes as a single argument in the end.
|
// Note: the entire command goes as a single argument. It's passed verbatim to `sh -c`.
|
||||||
// For example, this is a standard bind to toggle the screen reader (orca).
|
// For example, this is a standard bind to toggle the screen reader (orca).
|
||||||
Super+Alt+S hotkey-overlay-title=null { spawn "sh" "-c" "pkill orca || exec orca"; }
|
Super+Alt+S hotkey-overlay-title=null { spawn-sh "pkill orca || exec orca"; }
|
||||||
|
|
||||||
// Example volume keys mappings for PipeWire & WirePlumber.
|
// Example volume keys mappings for PipeWire & WirePlumber.
|
||||||
// The allow-when-locked=true property makes them work even when the session is locked.
|
// The allow-when-locked=true property makes them work even when the session is locked.
|
||||||
|
|||||||
+5
-1
@@ -44,7 +44,7 @@ use crate::layout::scrolling::ScrollDirection;
|
|||||||
use crate::layout::{ActivateWindow, LayoutElement as _};
|
use crate::layout::{ActivateWindow, LayoutElement as _};
|
||||||
use crate::niri::{CastTarget, PointerVisibility, State};
|
use crate::niri::{CastTarget, PointerVisibility, State};
|
||||||
use crate::ui::screenshot_ui::ScreenshotUi;
|
use crate::ui::screenshot_ui::ScreenshotUi;
|
||||||
use crate::utils::spawning::spawn;
|
use crate::utils::spawning::{spawn, spawn_sh};
|
||||||
use crate::utils::{center, get_monotonic_time, ResizeEdge};
|
use crate::utils::{center, get_monotonic_time, ResizeEdge};
|
||||||
|
|
||||||
pub mod backend_ext;
|
pub mod backend_ext;
|
||||||
@@ -595,6 +595,10 @@ impl State {
|
|||||||
let (token, _) = self.niri.activation_state.create_external_token(None);
|
let (token, _) = self.niri.activation_state.create_external_token(None);
|
||||||
spawn(command, Some(token.clone()));
|
spawn(command, Some(token.clone()));
|
||||||
}
|
}
|
||||||
|
Action::SpawnSh(command) => {
|
||||||
|
let (token, _) = self.niri.activation_state.create_external_token(None);
|
||||||
|
spawn_sh(command, Some(token.clone()));
|
||||||
|
}
|
||||||
Action::DoScreenTransition(delay_ms) => {
|
Action::DoScreenTransition(delay_ms) => {
|
||||||
self.backend.with_primary_renderer(|renderer| {
|
self.backend.with_primary_renderer(|renderer| {
|
||||||
self.niri.do_screen_transition(renderer, delay_ms);
|
self.niri.do_screen_transition(renderer, delay_ms);
|
||||||
|
|||||||
+6
-2
@@ -20,8 +20,8 @@ use niri::dbus;
|
|||||||
use niri::ipc::client::handle_msg;
|
use niri::ipc::client::handle_msg;
|
||||||
use niri::niri::State;
|
use niri::niri::State;
|
||||||
use niri::utils::spawning::{
|
use niri::utils::spawning::{
|
||||||
spawn, store_and_increase_nofile_rlimit, CHILD_DISPLAY, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE,
|
spawn, spawn_sh, store_and_increase_nofile_rlimit, CHILD_DISPLAY, CHILD_ENV,
|
||||||
REMOVE_ENV_RUST_LIB_BACKTRACE,
|
REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
|
||||||
};
|
};
|
||||||
use niri::utils::{cause_panic, version, watcher, xwayland, IS_SYSTEMD_SERVICE};
|
use niri::utils::{cause_panic, version, watcher, xwayland, IS_SYSTEMD_SERVICE};
|
||||||
use niri_config::ConfigPath;
|
use niri_config::ConfigPath;
|
||||||
@@ -151,6 +151,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||||
|
let spawn_at_startup_sh = mem::take(&mut config.spawn_at_startup_sh);
|
||||||
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
|
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
|
||||||
|
|
||||||
store_and_increase_nofile_rlimit();
|
store_and_increase_nofile_rlimit();
|
||||||
@@ -237,6 +238,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
for elem in spawn_at_startup {
|
for elem in spawn_at_startup {
|
||||||
spawn(elem.command, None);
|
spawn(elem.command, None);
|
||||||
}
|
}
|
||||||
|
for elem in spawn_at_startup_sh {
|
||||||
|
spawn_sh(elem.command, None);
|
||||||
|
}
|
||||||
|
|
||||||
// Show the config error notification right away if needed.
|
// Show the config error notification right away if needed.
|
||||||
if config_errored {
|
if config_errored {
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ fn render(
|
|||||||
|
|
||||||
// Add the spawn actions.
|
// Add the spawn actions.
|
||||||
for bind in binds.iter().filter(|bind| {
|
for bind in binds.iter().filter(|bind| {
|
||||||
matches!(bind.action, Action::Spawn(_))
|
matches!(bind.action, Action::Spawn(_) | Action::SpawnSh(_))
|
||||||
// Only show binds with Mod or Super to filter out stuff like volume up/down.
|
// Only show binds with Mod or Super to filter out stuff like volume up/down.
|
||||||
&& (bind.key.modifiers.contains(Modifiers::COMPOSITOR)
|
&& (bind.key.modifiers.contains(Modifiers::COMPOSITOR)
|
||||||
|| bind.key.modifiers.contains(Modifiers::SUPER))
|
|| bind.key.modifiers.contains(Modifiers::SUPER))
|
||||||
@@ -447,6 +447,11 @@ fn action_name(action: &Action) -> String {
|
|||||||
"Spawn <span face='monospace' bgcolor='#000000'>{}</span>",
|
"Spawn <span face='monospace' bgcolor='#000000'>{}</span>",
|
||||||
args.first().unwrap_or(&String::new())
|
args.first().unwrap_or(&String::new())
|
||||||
),
|
),
|
||||||
|
Action::SpawnSh(command) => format!(
|
||||||
|
"Spawn <span face='monospace' bgcolor='#000000'>{}</span>",
|
||||||
|
// Fairly crude but should get the job done in most cases.
|
||||||
|
command.split_ascii_whitespace().next().unwrap_or("")
|
||||||
|
),
|
||||||
_ => String::from("FIXME: Unknown"),
|
_ => String::from("FIXME: Unknown"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>, token: Option<Xd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns the command through the shell.
|
||||||
|
///
|
||||||
|
/// We hardcode `sh -c`, consistent with other compositors:
|
||||||
|
///
|
||||||
|
/// - https://github.com/swaywm/sway/blob/b3dcde8d69c3f1304b076968a7a64f54d0c958be/sway/commands/exec_always.c#L64
|
||||||
|
/// - https://github.com/hyprwm/Hyprland/blob/1ac1ff457ab8ef1ae6a8f2ab17ee7965adfa729f/src/managers/KeybindManager.cpp#L987
|
||||||
|
pub fn spawn_sh(command: String, token: Option<XdgActivationToken>) {
|
||||||
|
spawn(vec![String::from("sh"), String::from("-c"), command], token);
|
||||||
|
}
|
||||||
|
|
||||||
fn spawn_sync(
|
fn spawn_sync(
|
||||||
command: impl AsRef<OsStr>,
|
command: impl AsRef<OsStr>,
|
||||||
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
|
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
|
||||||
|
|||||||
Reference in New Issue
Block a user