-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
-
441
-
442
-
443
-
444
-
445
-
446
-
447
-
448
-
449
-
450
-
451
-
452
-
453
-
454
-
455
-
456
-
457
-
458
-
459
-
460
-
461
-
462
-
463
-
464
-
465
-
466
-
467
-
468
-
469
-
470
-
471
-
472
-
473
-
474
-
475
-
476
-
477
-
478
-
479
-
480
-
481
-
482
-
483
-
484
-
485
-
486
-
487
-
488
-
489
-
490
-
491
-
492
-
493
-
494
# Copyright 2025 Shota FUJI <pockawoooh@gmail.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
#
# SPDX-License-Identifier: 0BSD
{ config, lib, pkgs, ... }:
let
cfg = config.features.wayland-de.niri;
output = lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.nonEmptyStr;
description = "Display output name, you can obtain from `niri msg outputs`";
};
scale = lib.mkOption {
type = lib.types.float;
description = "DPI";
default = 1.0;
};
};
};
serializeOutput = background-color: o:
''
output "${o.name}" {
scale ${builtins.toString o.scale}
${if background-color == null then "//no bg" else "background-color \"${background-color}\""}
}
'';
serializeSpawnArg = a:
builtins.concatStringsSep " " (builtins.map (s: "\"${s}\"") a);
in
{
options = {
features.wayland-de.niri = {
enable = lib.mkEnableOption "Niri";
outputs = lib.mkOption {
type = lib.types.listOf output;
default = [ ];
};
background-color = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
input = {
keyboard = {
repeat-delay = lib.mkOption {
type = lib.types.ints.unsigned;
default = 200;
};
repeat-rate = lib.mkOption {
type = lib.types.ints.unsigned;
default = 25;
};
};
};
spawn-at-startup = lib.mkOption {
type = lib.types.listOf (lib.types.listOf lib.types.nonEmptyStr);
description = ''
`spawn-at-startup` accepts a path to the program binary as the first argument, followed by arguments to the program.
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.
'';
default = [ ];
};
overview = {
backdrop-color = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
};
layout = {
gaps = lib.mkOption {
type = lib.types.ints.unsigned;
description = "Set gaps around windows in logical pixels.";
default = 16;
};
center-focused-column = lib.mkOption {
type = lib.types.enum [ "never" "always" "on-overflow" ];
description = ''
When to center a column when changing focus, options are:
- "never", default behavior, focusing an off-screen column will keep at the left
or right edge of the screen.
- "always", the focused column will always be centered.
- "on-overflow", focusing a column will center it if it doesn't fit
together with the previously focused column.
'';
default = "never";
};
focus-ring = {
width = lib.mkOption {
type = lib.types.ints.unsigned;
description = "How many logical pixels the ring extends out from the windows.";
default = 3;
};
active-color = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
Color of the ring on the active monitor.
Colors can be set in a variety of ways:
- CSS named colors: "red"
- RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
- CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
You can also use gradients. They take precedence over solid colors.
Gradients are rendered the same as CSS linear-gradient(angle, from, to).
The angle is the same as in linear-gradient, and is optional,
defaulting to 180 (top-to-bottom gradient).
You can use any CSS linear-gradient tool on the web to set these up.
Changing the color space is also supported, check the wiki for more info.
active-gradient from="#80c8ff" to="#bbddff" angle=45
You can also color the gradient relative to the entire view
of the workspace, rather than relative to just the window itself.
To do that, set relative-to="workspace-view".
inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
'';
default = "#7fc8ff";
};
inactive-color = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
Color of the ring on inactive monitors.
Colors can be set in a variety of ways:
- CSS named colors: "red"
- RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
- CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
You can also use gradients. They take precedence over solid colors.
Gradients are rendered the same as CSS linear-gradient(angle, from, to).
The angle is the same as in linear-gradient, and is optional,
defaulting to 180 (top-to-bottom gradient).
You can use any CSS linear-gradient tool on the web to set these up.
Changing the color space is also supported, check the wiki for more info.
active-gradient from="#80c8ff" to="#bbddff" angle=45
You can also color the gradient relative to the entire view
of the workspace, rather than relative to just the window itself.
To do that, set relative-to="workspace-view".
inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
'';
default = "#505050";
};
};
border = {
width = lib.mkOption {
type = lib.types.ints.unsigned;
description = "How many logical pixels the ring extends out from the windows.";
default = 3;
};
active-color = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
Color of the border on the active monitor.
Colors can be set in a variety of ways:
- CSS named colors: "red"
- RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
- CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
You can also use gradients. They take precedence over solid colors.
Gradients are rendered the same as CSS linear-gradient(angle, from, to).
The angle is the same as in linear-gradient, and is optional,
defaulting to 180 (top-to-bottom gradient).
You can use any CSS linear-gradient tool on the web to set these up.
Changing the color space is also supported, check the wiki for more info.
active-gradient from="#80c8ff" to="#bbddff" angle=45
You can also color the gradient relative to the entire view
of the workspace, rather than relative to just the window itself.
To do that, set relative-to="workspace-view".
inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
'';
default = "#7fc8ff";
};
inactive-color = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
Color of the border on inactive monitors.
Colors can be set in a variety of ways:
- CSS named colors: "red"
- RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
- CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
You can also use gradients. They take precedence over solid colors.
Gradients are rendered the same as CSS linear-gradient(angle, from, to).
The angle is the same as in linear-gradient, and is optional,
defaulting to 180 (top-to-bottom gradient).
You can use any CSS linear-gradient tool on the web to set these up.
Changing the color space is also supported, check the wiki for more info.
active-gradient from="#80c8ff" to="#bbddff" angle=45
You can also color the gradient relative to the entire view
of the workspace, rather than relative to just the window itself.
To do that, set relative-to="workspace-view".
inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
'';
default = "#505050";
};
};
struts = {
left = lib.mkOption {
type = lib.types.int;
description = ''
Struts shrink the area occupied by windows, similarly to layer-shell panels.
You can think of them as a kind of outer gaps. They are set in logical pixels.
Left and right struts will cause the next window to the side to always be visible.
'';
default = 0;
};
right = lib.mkOption {
type = lib.types.int;
description = ''
Struts shrink the area occupied by windows, similarly to layer-shell panels.
You can think of them as a kind of outer gaps. They are set in logical pixels.
Left and right struts will cause the next window to the side to always be visible.
'';
default = 0;
};
top = lib.mkOption {
type = lib.types.int;
description = ''
Struts shrink the area occupied by windows, similarly to layer-shell panels.
You can think of them as a kind of outer gaps. They are set in logical pixels.
Top and bottom struts will simply add outer gaps in addition to the area occupied by
layer-shell panels and regular gaps.
'';
default = 0;
};
bottom = lib.mkOption {
type = lib.types.int;
description = ''
Struts shrink the area occupied by windows, similarly to layer-shell panels.
You can think of them as a kind of outer gaps. They are set in logical pixels.
Top and bottom struts will simply add outer gaps in addition to the area occupied by
layer-shell panels and regular gaps.
'';
default = 0;
};
};
};
prefer-no-csd = lib.mkOption {
type = lib.types.bool;
description = ''
Ask the clients to omit their client-side decorations if possible.
If the client will specifically ask for CSD, the request will be honored.
Additionally, clients will be informed that they are tiled, removing some client-side rounded corners.
This option will also fix border/focus ring drawing behind some semitransparent windows.
After enabling or disabling this, you need to restart the apps for this to take effect.
'';
default = true;
};
screenshot-path = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
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.
'';
default = "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png";
};
window-rule-all = {
corner-radius = lib.mkOption {
type = lib.types.ints.unsigned;
default = 8;
};
};
hotkey-overlay = {
skip-at-startup = lib.mkOption {
type = lib.types.bool;
default = true;
};
};
};
};
config = lib.mkIf config.features.wayland-de.enable {
xdg.configFile."niri/config.kdl" = {
# https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
text = ''
input {
keyboard {
repeat-delay ${builtins.toString cfg.input.keyboard.repeat-delay}
repeat-rate ${builtins.toString cfg.input.keyboard.repeat-rate}
}
// This section includes libinput settings.
// Omitting settings disables them, or leaves them at their default values.
touchpad {
natural-scroll
accel-speed 0.2
accel-profile "adaptive"
scroll-method "two-finger"
scroll-factor 0.3
click-method "clickfinger"
}
}
${lib.strings.concatStringsSep "\n" (builtins.map (serializeOutput cfg.background-color) cfg.outputs)}
overview {
${if cfg.overview.backdrop-color != null
then "backdrop-color \"${cfg.overview.backdrop-color}\""
else "// No backdrop-color"
}
}
layout {
gaps ${builtins.toString cfg.layout.gaps}
center-focused-column "${cfg.layout.center-focused-column}"
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.25
proportion 0.5
proportion 0.75
}
preset-window-heights {
proportion 0.25
proportion 0.5
proportion 0.75
}
// You can change the default width of the new windows.
// If you leave the brackets empty, the windows themselves will decide their initial width.
default-column-width {
proportion 0.5
}
focus-ring {
width ${builtins.toString cfg.layout.focus-ring.width}
active-color "${cfg.layout.focus-ring.active-color}"
inactive-color "${cfg.layout.focus-ring.inactive-color}"
}
border {
width ${builtins.toString cfg.layout.border.width}
active-color "${cfg.layout.border.active-color}"
inactive-color "${cfg.layout.border.inactive-color}"
}
shadow {
on
softness 40
spread 5
}
struts {
left ${builtins.toString cfg.layout.struts.left}
right ${builtins.toString cfg.layout.struts.right}
top ${builtins.toString cfg.layout.struts.top}
bottom ${builtins.toString cfg.layout.struts.bottom}
}
}
${if cfg.prefer-no-csd then "" else "//"}prefer-no-csd
screenshot-path "${cfg.screenshot-path}"
animations {
}
// Open the Firefox picture-in-picture player as floating by default.
window-rule {
// This app-id regular expression will work for both:
// - host Firefox (app-id is "firefox")
// - Flatpak Firefox (app-id is "org.mozilla.firefox")
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
open-floating true
}
window-rule {
match app-id="^com\\.mitchellh\\.ghostty$"
default-column-width {
proportion 0.25
}
}
// Enable rounded corners for all windows.
window-rule {
geometry-corner-radius ${builtins.toString cfg.window-rule-all.corner-radius}
clip-to-geometry true
}
hotkey-overlay {
${if cfg.hotkey-overlay.skip-at-startup then "" else "//" }skip-at-startup
}
${builtins.concatStringsSep "\n" (builtins.map (a: "spawn-at-startup ${serializeSpawnArg a}") cfg.spawn-at-startup)}
binds {
Mod+Shift+Slash { show-hotkey-overlay; }
Mod+T { spawn "${config.lib.nixGL.wrap pkgs.ghostty}/bin/ghostty"; }
Mod+X { spawn "swaylock"; }
Mod+Q { close-window; }
Mod+Space { spawn "${pkgs.walker}/bin/walker" "--modules" "applications,commands,calc,power"; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down-or-column-right; }
Mod+K { focus-window-up-or-column-left; }
Mod+L { focus-column-right; }
Mod+Ctrl+H { move-column-left; }
Mod+Ctrl+J { consume-or-expel-window-left; }
Mod+Ctrl+K { consume-or-expel-window-right; }
Mod+Ctrl+L { move-column-right; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Comma { consume-window-into-column; }
Mod+Period { expel-window-from-column; }
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+C { center-column; }
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
Mod+V { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+Shift+E { quit; }
Ctrl+Alt+Delete { quit; }
Mod+Shift+P { power-off-monitors; }
}
'';
};
};
}