Skip to content

Commit 063ac1e

Browse files
Switch to separate press and release action config
1 parent 695dac6 commit 063ac1e

File tree

6 files changed

+370
-174
lines changed

6 files changed

+370
-174
lines changed

docs/wiki/Configuration:-Key-Bindings.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,26 +81,44 @@ binds {
8181

8282
This is mostly useful for the scroll bindings.
8383

84-
### Release bindings
84+
### Press and Release bindings
8585

8686
<sup>Since: next release</sup>
8787

88-
Binds can be set to trigger on release instead of on the initial key press. This is mostly useful when you want to bind a modifier key to an action, as it avoids unwanted triggering when you're trying to use other binds involving that modifier.
88+
Binds can be set to trigger on key press, on key release, or both. By default, binds trigger on key press. You can specify the timing using `press {}` and `release {}` blocks:
8989

9090
```kdl
9191
binds {
92-
Mod release=true { toggle-overview; }
92+
// Trigger on press (default behavior)
93+
Mod+T { spawn "alacritty"; }
94+
95+
// Trigger on release
96+
Mod { release { toggle-overview; } }
97+
98+
// Trigger on both press and release with different actions
99+
Mod+Shift+Q repeat=false {
100+
press { spawn "notify-send" "Pressed"; }
101+
release { spawn "notify-send" "Released"; }
102+
}
93103
}
94104
```
95105

96-
These will normally only trigger if no other keys were released and no keys or mouse buttons were pressed after the bound key was pressed. If you want one to always trigger you should also set `allow-invalidation=false`.
106+
Release bindings are mostly useful when you want to bind a modifier key to an action, as it avoids unwanted triggering when you're trying to use other binds involving that modifier.
107+
108+
Release binds will normally only trigger if no other keys were released and no keys or mouse buttons were pressed after the bound key was pressed. If you want a release bind to always trigger regardless, set `allow-invalidation=false`:
109+
110+
```kdl
111+
binds {
112+
Mod allow-invalidation=false { release { toggle-overview; } }
113+
}
114+
```
97115

98116
Note that the modifier state is updated before binds are evaluated, so if you want to configure a modifier key as both a normal bind and a release bind the entries are slightly different.
99117

100118
```kdl
101119
binds {
102120
Alt+Ctrl+Control_L repeat=false { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "0"; }
103-
Alt+Control_L release=true allow-invalidation=false { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "1"; }
121+
Alt+Control_L allow-invalidation=false { release { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "1"; } }
104122
}
105123
```
106124

niri-config/src/binds.rs

Lines changed: 177 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,38 @@ pub struct Binds(pub Vec<Bind>);
2121
#[derive(Debug, Clone, PartialEq)]
2222
pub struct Bind {
2323
pub key: Key,
24-
pub action: Action,
24+
pub press_action: Option<Action>,
25+
pub release_action: Option<Action>,
2526
pub repeat: bool,
26-
pub release: bool,
2727
pub cooldown: Option<Duration>,
2828
pub allow_when_locked: bool,
2929
pub allow_inhibiting: bool,
3030
pub allow_invalidation: bool,
3131
pub hotkey_overlay_title: Option<Option<String>>,
3232
}
3333

34+
impl Bind {
35+
pub fn has_press(&self) -> bool {
36+
self.press_action.is_some()
37+
}
38+
39+
pub fn has_release(&self) -> bool {
40+
self.release_action.is_some()
41+
}
42+
43+
pub fn is_release_only(&self) -> bool {
44+
self.press_action.is_none() && self.release_action.is_some()
45+
}
46+
47+
pub fn action_for(&self, pressed: bool) -> Option<&Action> {
48+
if pressed {
49+
self.press_action.as_ref()
50+
} else {
51+
self.release_action.as_ref()
52+
}
53+
}
54+
}
55+
3456
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
3557
pub struct Key {
3658
pub trigger: Trigger,
@@ -773,7 +795,6 @@ where
773795
expect_only_children(node, ctx);
774796

775797
let mut seen_keys = HashSet::new();
776-
let mut seen_keys_release = HashSet::new();
777798

778799
let mut binds = Vec::new();
779800

@@ -808,17 +829,7 @@ where
808829
// why does *that one* especially, require a DecodeError?
809830
//
810831
// anyways if you can make it format nicely, definitely do fix this
811-
if bind.release {
812-
if seen_keys_release.insert(bind.key) {
813-
binds.push(bind);
814-
} else {
815-
ctx.emit_error(DecodeError::unexpected(
816-
&child.node_name,
817-
"keybind",
818-
"duplicate release keybind",
819-
));
820-
}
821-
} else if seen_keys.insert(bind.key) {
832+
if seen_keys.insert(bind.key) {
822833
binds.push(bind);
823834
} else {
824835
ctx.emit_error(DecodeError::unexpected(
@@ -865,21 +876,18 @@ where
865876
.map_err(|e| DecodeError::conversion(&node.node_name, e.wrap_err("invalid keybind")))?;
866877

867878
let mut repeat = true;
868-
let mut release = false;
869879
let mut cooldown = None;
870880
let mut allow_when_locked = false;
871881
let mut allow_when_locked_node = None;
872882
let mut allow_inhibiting = true;
873883
let mut allow_invalidation = true;
874884
let mut hotkey_overlay_title = None;
885+
875886
for (name, val) in &node.properties {
876887
match &***name {
877888
"repeat" => {
878889
repeat = knuffel::traits::DecodeScalar::decode(val, ctx)?;
879890
}
880-
"release" => {
881-
release = knuffel::traits::DecodeScalar::decode(val, ctx)?;
882-
}
883891
"cooldown-ms" => {
884892
cooldown = Some(Duration::from_millis(
885893
knuffel::traits::DecodeScalar::decode(val, ctx)?,
@@ -908,73 +916,179 @@ where
908916
}
909917
}
910918

911-
let mut children = node.children();
912-
913919
// If the action is invalid but the key is fine, we still want to return something.
914920
// That way, the parent can handle the existence of duplicate keybinds,
915921
// even if their contents are not valid.
916922
let dummy = Self {
917923
key,
918-
action: Action::Spawn(vec![]),
924+
press_action: Some(Action::Spawn(vec![])),
925+
release_action: None,
919926
repeat: true,
920-
release: false,
921927
cooldown: None,
922928
allow_when_locked: false,
923929
allow_inhibiting: true,
924930
allow_invalidation: true,
925931
hotkey_overlay_title: None,
926932
};
927933

928-
if let Some(child) = children.next() {
929-
for unwanted_child in children {
930-
ctx.emit_error(DecodeError::unexpected(
931-
unwanted_child,
932-
"node",
933-
"only one action is allowed per keybind",
934-
));
935-
}
936-
match Action::decode_node(child, ctx) {
937-
Ok(action) => {
938-
if !matches!(action, Action::Spawn(_) | Action::SpawnSh(_)) {
939-
if let Some(node) = allow_when_locked_node {
940-
ctx.emit_error(DecodeError::unexpected(
941-
node,
942-
"property",
943-
"allow-when-locked can only be set on spawn binds",
944-
));
934+
let mut press_action: Option<Action> = None;
935+
let mut release_action: Option<Action> = None;
936+
let mut has_press_section = false;
937+
let mut has_release_section = false;
938+
939+
for child in node.children() {
940+
let child_name = &*child.node_name;
941+
942+
if child_name.as_ref() == "press" {
943+
if has_press_section {
944+
ctx.emit_error(DecodeError::unexpected(
945+
&child.node_name,
946+
"section",
947+
"duplicate `press` section",
948+
));
949+
continue;
950+
}
951+
has_press_section = true;
952+
953+
let mut press_children = child.children();
954+
if let Some(action_child) = press_children.next() {
955+
for unwanted_child in press_children {
956+
ctx.emit_error(DecodeError::unexpected(
957+
unwanted_child,
958+
"node",
959+
"only one action is allowed per keybind",
960+
));
961+
}
962+
match Action::decode_node(action_child, ctx) {
963+
Ok(action) => {
964+
press_action = Some(action);
965+
}
966+
Err(e) => {
967+
ctx.emit_error(e);
968+
}
969+
}
970+
} else {
971+
ctx.emit_error(DecodeError::missing(
972+
child,
973+
"expected an action for this press section",
974+
));
975+
}
976+
} else if child_name.as_ref() == "release" {
977+
if has_release_section {
978+
ctx.emit_error(DecodeError::unexpected(
979+
&child.node_name,
980+
"section",
981+
"duplicate `release` section",
982+
));
983+
continue;
984+
}
985+
has_release_section = true;
986+
987+
let mut release_children = child.children();
988+
if let Some(action_child) = release_children.next() {
989+
for unwanted_child in release_children {
990+
ctx.emit_error(DecodeError::unexpected(
991+
unwanted_child,
992+
"node",
993+
"only one action is allowed per keybind",
994+
));
995+
}
996+
match Action::decode_node(action_child, ctx) {
997+
Ok(action) => {
998+
release_action = Some(action);
999+
}
1000+
Err(e) => {
1001+
ctx.emit_error(e);
9451002
}
9461003
}
1004+
} else {
1005+
ctx.emit_error(DecodeError::missing(
1006+
child,
1007+
"expected an action for this release section",
1008+
));
1009+
}
1010+
} else {
1011+
if has_press_section || has_release_section {
1012+
ctx.emit_error(DecodeError::unexpected(
1013+
&child.node_name,
1014+
"node",
1015+
"cannot mix direct actions with press/release sections",
1016+
));
1017+
continue;
1018+
}
9471019

948-
// The toggle-inhibit action must always be uninhibitable.
949-
// Otherwise, it would be impossible to trigger it.
950-
if matches!(action, Action::ToggleKeyboardShortcutsInhibit) {
951-
allow_inhibiting = false;
1020+
if press_action.is_some() {
1021+
ctx.emit_error(DecodeError::unexpected(
1022+
&child.node_name,
1023+
"node",
1024+
"only one action is allowed per keybind",
1025+
));
1026+
continue;
1027+
}
1028+
1029+
match Action::decode_node(child, ctx) {
1030+
Ok(action) => {
1031+
press_action = Some(action);
9521032
}
1033+
Err(e) => {
1034+
ctx.emit_error(e);
1035+
}
1036+
}
1037+
}
1038+
}
9531039

954-
Ok(Self {
955-
key,
956-
action,
957-
repeat,
958-
release,
959-
cooldown,
960-
allow_when_locked,
961-
allow_inhibiting,
962-
allow_invalidation,
963-
hotkey_overlay_title,
964-
})
1040+
if press_action.is_none() && release_action.is_none() {
1041+
if !has_press_section && !has_release_section {
1042+
ctx.emit_error(DecodeError::missing(
1043+
node,
1044+
"expected an action for this keybind",
1045+
));
1046+
}
1047+
return Ok(dummy);
1048+
}
1049+
1050+
if let Some(ref action) = press_action {
1051+
if !matches!(action, Action::Spawn(_) | Action::SpawnSh(_)) {
1052+
if let Some(node) = allow_when_locked_node {
1053+
ctx.emit_error(DecodeError::unexpected(
1054+
node,
1055+
"property",
1056+
"allow-when-locked can only be set on spawn binds",
1057+
));
9651058
}
966-
Err(e) => {
967-
ctx.emit_error(e);
968-
Ok(dummy)
1059+
}
1060+
}
1061+
if let Some(ref action) = release_action {
1062+
if !matches!(action, Action::Spawn(_) | Action::SpawnSh(_)) {
1063+
if let Some(node) = allow_when_locked_node {
1064+
ctx.emit_error(DecodeError::unexpected(
1065+
node,
1066+
"property",
1067+
"allow-when-locked can only be set on spawn binds",
1068+
));
9691069
}
9701070
}
971-
} else {
972-
ctx.emit_error(DecodeError::missing(
973-
node,
974-
"expected an action for this keybind",
975-
));
976-
Ok(dummy)
9771071
}
1072+
1073+
// The toggle-inhibit action must always be uninhibitable.
1074+
// Otherwise, it would be impossible to trigger it.
1075+
if matches!(press_action, Some(Action::ToggleKeyboardShortcutsInhibit))
1076+
|| matches!(release_action, Some(Action::ToggleKeyboardShortcutsInhibit))
1077+
{
1078+
allow_inhibiting = false;
1079+
}
1080+
1081+
Ok(Self {
1082+
key,
1083+
press_action,
1084+
release_action,
1085+
repeat,
1086+
cooldown,
1087+
allow_when_locked,
1088+
allow_inhibiting,
1089+
allow_invalidation,
1090+
hotkey_overlay_title,
1091+
})
9781092
}
9791093
}
9801094

niri-config/src/recent_windows.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ impl From<MruBind> for Bind {
148148
fn from(x: MruBind) -> Self {
149149
Self {
150150
key: x.key,
151-
action: Action::from(x.action),
151+
press_action: Some(Action::from(x.action)),
152+
release_action: None,
152153
repeat: true,
153-
release: false,
154154
cooldown: None,
155155
allow_when_locked: false,
156156
allow_inhibiting: x.allow_inhibiting,

0 commit comments

Comments
 (0)