Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion docs/wiki/Configuration:-Gestures.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ gestures {
// top-right
// bottom-left
// bottom-right

// You can also run a custom action instead of toggling the overview.
// top-right {
// power-off-monitors
// }
}
}
```
Expand Down Expand Up @@ -85,8 +90,9 @@ gestures {

<sup>Since: 25.05</sup>

Put your mouse at the very top-left corner of a monitor to toggle the overview.
Put your mouse at a hot corner of a monitor to run an action.
Also works during drag-and-dropping something.
By default, the top-left hot corner toggles the overview.

`off` disables the hot corners.

Expand All @@ -101,6 +107,7 @@ gestures {

<sup>Since: 25.11</sup> You can choose specific hot corners by name: `top-left`, `top-right`, `bottom-left`, `bottom-right`.
If no corners are explicitly set, the top-left corner will be active by default.
Bare corner names toggle the overview for compatibility with previous versions.

```kdl
// Enable the top-right and bottom-right hot corners.
Expand All @@ -112,4 +119,17 @@ gestures {
}
```

You can set a hot corner to run any bind action by putting exactly one action inside its block.

```kdl
// Power off monitors when the pointer enters the top-right hot corner.
gestures {
hot-corners {
top-right {
power-off-monitors
}
}
}
```

You can also customize hot corners per-output [in the output config](./Configuration:-Outputs.md#hot-corners).
18 changes: 17 additions & 1 deletion docs/wiki/Configuration:-Outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ output "eDP-1" {
// top-right
// bottom-left
// bottom-right

// You can also run a custom action instead of toggling the overview.
// top-right {
// power-off-monitors
// }
}

layout {
Expand Down Expand Up @@ -286,7 +291,9 @@ output "HDMI-A-1" {
Customize the hot corners for this output.
By default, hot corners [in the gestures settings](./Configuration:-Gestures.md#hot-corners) are used for all outputs.

Hot corners toggle the overview when you put your mouse at the very corner of a monitor.
Hot corners run an action when you put your mouse at the very corner of a monitor.
Bare corner names toggle the overview for compatibility with previous versions.
A corner block with exactly one action runs that action instead.

`off` will disable the hot corners on this output, and writing specific corners will enable only those hot corners on this output.

Expand All @@ -305,6 +312,15 @@ output "DP-2" {
off
}
}

// Power off monitors from the top-right hot corner on eDP-1.
output "eDP-1" {
hot-corners {
top-right {
power-off-monitors
}
}
}
```

### Layout config overrides
Expand Down
5 changes: 3 additions & 2 deletions docs/wiki/Gestures.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ This will bring a floating window to the top for example.

In the overview, you can also hold the mouse over a workspace to switch to it.

#### Hot Corner to Toggle the Overview
#### Hot Corners

<sup>Since: 25.05</sup>

Put your mouse at the very top-left corner of a monitor to toggle the overview.
Put your mouse at a hot corner of a monitor to run its configured action.
By default, the top-left hot corner toggles the overview.
Also works during drag-and-dropping something.
4 changes: 2 additions & 2 deletions docs/wiki/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995

</video>

Open it with the `toggle-overview` bind, via the top-left hot corner, or using a touchpad four-finger swipe up.
Open it with the `toggle-overview` bind, via the default top-left hot corner, or using a touchpad four-finger swipe up.
While in the overview, all keyboard shortcuts keep working, while pointing devices get easier:

- Mouse: left click and drag windows to move them, right click and drag to scroll workspaces left/right, scroll to switch workspaces (no holding Mod required).
Expand All @@ -25,7 +25,7 @@ While in the overview, all keyboard shortcuts keep working, while pointing devic
> Put your bar on the *top* layer.
Drag-and-drop will scroll the workspaces up/down in the overview, and will activate a workspace when holding it for a moment.
Combined with the hot corner, this lets you do a mouse-only DnD across workspaces.
Combined with the default hot corner, this lets you do a mouse-only DnD across workspaces.

<video controls src="https://github.com/user-attachments/assets/5f09c5b7-ff40-462b-8b9c-f1b8073a2cbb">

Expand Down
131 changes: 124 additions & 7 deletions niri-config/src/gestures.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
use knuffel::ast::SpannedNode;
use knuffel::decode::Context;
use knuffel::errors::DecodeError;
use knuffel::traits::ErrorSpan;

use crate::binds::Action;
use crate::utils::MergeWith;
use crate::FloatOrInt;

#[derive(Debug, Default, Clone, Copy, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Gestures {
pub dnd_edge_view_scroll: DndEdgeViewScroll,
pub dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch,
pub hot_corners: HotCorners,
}

#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct GesturesPart {
#[knuffel(child)]
pub dnd_edge_view_scroll: Option<DndEdgeViewScrollPart>,
Expand Down Expand Up @@ -97,16 +103,127 @@ impl MergeWith<DndEdgeWorkspaceSwitchPart> for DndEdgeWorkspaceSwitch {
}
}

#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct HotCorners {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub top_left: bool,
pub top_left: Option<HotCorner>,
#[knuffel(child)]
pub top_right: bool,
pub top_right: Option<HotCorner>,
#[knuffel(child)]
pub bottom_left: bool,
pub bottom_left: Option<HotCorner>,
#[knuffel(child)]
pub bottom_right: bool,
pub bottom_right: Option<HotCorner>,
}

#[derive(Debug, Default, Clone, PartialEq)]
pub struct HotCorner {
pub action: Option<Action>,
}

impl HotCorners {

pub fn has_any_explicit_corner(&self) -> bool {
self.top_left.is_some()
|| self.top_right.is_some()
|| self.bottom_left.is_some()
|| self.bottom_right.is_some()
}

pub fn action_top_left(&self) -> Option<Action> {
if self.off {
return None;
}

if let Some(corner) = &self.top_left {
return Some(corner.action.clone().unwrap_or(Action::ToggleOverview));
}

if !self.has_any_explicit_corner() {
return Some(Action::ToggleOverview);
}

None
}

pub fn action_top_right(&self) -> Option<Action> {
if self.off {
return None;
}

self.top_right
.as_ref()
.map(|corner| corner.action.clone().unwrap_or(Action::ToggleOverview))
}

pub fn action_bottom_left(&self) -> Option<Action> {
if self.off {
return None;
}

self.bottom_left
.as_ref()
.map(|corner| corner.action.clone().unwrap_or(Action::ToggleOverview))
}

pub fn action_bottom_right(&self) -> Option<Action> {
if self.off {
return None;
}

self.bottom_right
.as_ref()
.map(|corner| corner.action.clone().unwrap_or(Action::ToggleOverview))
}
}

impl<S: ErrorSpan> knuffel::Decode<S> for HotCorner {
fn decode_node(node: &SpannedNode<S>, ctx: &mut Context<S>) -> Result<Self, DecodeError<S>> {
if let Some(type_name) = &node.type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}

for val in node.arguments.iter() {
ctx.emit_error(DecodeError::unexpected(
&val.literal,
"argument",
"no arguments expected for this node",
));
}

for (name, _) in &node.properties {
ctx.emit_error(DecodeError::unexpected(
name,
"property",
format!("unexpected property `{}`", name.escape_default()),
));
}

let mut children = node.children();
let action = if let Some(child) = children.next() {
for unwanted_child in children {
ctx.emit_error(DecodeError::unexpected(
unwanted_child,
"node",
"only one action is allowed per hot corner",
));
}
match <Action as knuffel::Decode<S>>::decode_node(child, ctx) {
Ok(action) => Some(action),
Err(e) => {
ctx.emit_error(e);
None
}
}
} else {
None
};

Ok(Self { action })
}
}
49 changes: 41 additions & 8 deletions niri-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,13 @@ mod tests {
trigger-width 10
max-speed 50
}

hot-corners {
top-left
top-right {
power-off-monitors
}
}
}

environment {
Expand Down Expand Up @@ -1191,10 +1198,26 @@ mod tests {
hot_corners: Some(
HotCorners {
off: true,
top_left: true,
top_right: true,
bottom_left: true,
bottom_right: true,
top_left: Some(
HotCorner {
action: None,
},
),
top_right: Some(
HotCorner {
action: None,
},
),
bottom_left: Some(
HotCorner {
action: None,
},
),
bottom_right: Some(
HotCorner {
action: None,
},
),
},
),
layout: None,
Expand Down Expand Up @@ -1658,10 +1681,20 @@ mod tests {
},
hot_corners: HotCorners {
off: false,
top_left: false,
top_right: false,
bottom_left: false,
bottom_right: false,
top_left: Some(
HotCorner {
action: None,
},
),
top_right: Some(
HotCorner {
action: Some(
PowerOffMonitors,
),
},
),
bottom_left: None,
bottom_right: None,
},
},
overview: Overview {
Expand Down
11 changes: 10 additions & 1 deletion resources/default-config.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,15 @@ window-rule {
clip-to-geometry true
}

// Hot corners can trigger actions when you move the mouse into a monitor corner.
// Bare corner names toggle the overview. Corner blocks can run any bind action.
// gestures {
// hot-corners {
// top-left
// // top-right { power-off-monitors; }
// }
// }

binds {
// Keys consist of modifiers separated by + signs, followed by an XKB key name
// in the end. To find an XKB name for a particular key, you may use a program
Expand Down Expand Up @@ -394,7 +403,7 @@ binds {
XF86MonBrightnessDown allow-when-locked=true { spawn "brightnessctl" "--class=backlight" "set" "10%-"; }

// Open/close the Overview: a zoomed-out view of workspaces and windows.
// You can also move the mouse into the top-left hot corner,
// You can also move the mouse into the default top-left hot corner,
// or do a four-finger swipe up on a touchpad.
Mod+O repeat=false { toggle-overview; }

Expand Down
12 changes: 10 additions & 2 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2631,7 +2631,11 @@ impl State {
.with_grab(|_, grab| grab_allows_hot_corner(grab))
.unwrap_or(true)
{
self.niri.layout.toggle_overview();
if let Some((output, pos_within_output)) = self.niri.output_under(new_pos) {
if let Some(action) = self.niri.hot_corner_action(output, pos_within_output) {
self.do_action(action, false);
}
}
}
self.niri.pointer_inside_hot_corner = true;
}
Expand Down Expand Up @@ -2718,7 +2722,11 @@ impl State {
.with_grab(|_, grab| grab_allows_hot_corner(grab))
.unwrap_or(true)
{
self.niri.layout.toggle_overview();
if let Some((output, pos_within_output)) = self.niri.output_under(pos) {
if let Some(action) = self.niri.hot_corner_action(output, pos_within_output) {
self.do_action(action, false);
}
}
}
self.niri.pointer_inside_hot_corner = true;
}
Expand Down
Loading