-118
home/desktop/niri/config.kdl
-118
home/desktop/niri/config.kdl
···
1
-
prefer-no-csd
2
-
3
-
binds {
4
-
Mod+T { spawn "ghostty"; }
5
-
Mod+Space { spawn "cosmic-launcher"; }
6
-
Mod+Shift+Space { spawn "cosmic-app-library"; }
7
-
8
-
Mod+Escape repeat=false { toggle-keyboard-shortcuts-inhibit; }
9
-
10
-
Mod+O repeat=false { toggle-overview; }
11
-
Mod+W repeat=false { close-window; }
12
-
13
-
Mod+F { maximize-column; }
14
-
Mod+Shift+F { fullscreen-window; }
15
-
Mod+Z { center-column; }
16
-
17
-
Mod+Minus { set-column-width "-10%"; }
18
-
Mod+Equal { set-column-width "+10%"; }
19
-
20
-
Mod+Left { focus-column-left; }
21
-
Mod+Down { focus-window-down; }
22
-
Mod+Up { focus-window-up; }
23
-
Mod+Right { focus-column-right; }
24
-
Mod+H { focus-column-left; }
25
-
Mod+J { focus-window-or-workspace-down; }
26
-
Mod+K { focus-window-or-workspace-up; }
27
-
Mod+L { focus-column-right; }
28
-
29
-
Mod+Ctrl+Left { move-column-left; }
30
-
Mod+Ctrl+Down { move-window-down; }
31
-
Mod+Ctrl+Up { move-window-up; }
32
-
Mod+Ctrl+Right { move-column-right; }
33
-
Mod+Ctrl+H { move-column-left; }
34
-
Mod+Ctrl+J { move-window-down-or-to-workspace-down; }
35
-
Mod+Ctrl+K { move-window-up-or-to-workspace-up; }
36
-
Mod+Ctrl+L { move-column-right; }
37
-
38
-
Mod+Shift+Left { focus-monitor-left; }
39
-
Mod+Shift+Down { focus-monitor-down; }
40
-
Mod+Shift+Up { focus-monitor-up; }
41
-
Mod+Shift+Right { focus-monitor-right; }
42
-
Mod+Shift+H { focus-monitor-left; }
43
-
Mod+Shift+J { focus-monitor-down; }
44
-
Mod+Shift+K { focus-monitor-up; }
45
-
Mod+Shift+L { focus-monitor-right; }
46
-
47
-
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
48
-
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
49
-
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
50
-
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
51
-
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
52
-
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
53
-
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
54
-
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
55
-
56
-
Mod+Page_Down { focus-workspace-down; }
57
-
Mod+Page_Up { focus-workspace-up; }
58
-
Mod+U { focus-workspace-down; }
59
-
Mod+I { focus-workspace-up; }
60
-
Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
61
-
Mod+Ctrl+Page_Up { move-column-to-workspace-up; }
62
-
Mod+Ctrl+U { move-column-to-workspace-down; }
63
-
Mod+Ctrl+I { move-column-to-workspace-up; }
64
-
65
-
Mod+Shift+Page_Down { move-workspace-down; }
66
-
Mod+Shift+Page_Up { move-workspace-up; }
67
-
Mod+Shift+U { move-workspace-down; }
68
-
Mod+Shift+I { move-workspace-up; }
69
-
70
-
Mod+WheelScrollDown { focus-column-right; }
71
-
Mod+WheelScrollUp { focus-column-left; }
72
-
73
-
Mod+BracketLeft { consume-or-expel-window-left; }
74
-
Mod+BracketRight { consume-or-expel-window-right; }
75
-
Mod+Comma { consume-window-into-column; }
76
-
Mod+Period { expel-window-from-column; }
77
-
78
-
Print { screenshot; }
79
-
Ctrl+Print { screenshot-screen; }
80
-
Alt+Print { screenshot-window; }
81
-
}
82
-
83
-
input {
84
-
warp-mouse-to-focus
85
-
}
86
-
87
-
layout {
88
-
gaps 16
89
-
center-focused-column "never"
90
-
91
-
focus-ring {
92
-
width 2
93
-
}
94
-
}
95
-
96
-
cursor {
97
-
hide-after-inactive-ms 2000
98
-
}
99
-
100
-
hotkey-overlay {
101
-
hide-not-bound
102
-
}
103
-
104
-
window-rule {
105
-
match title="^Picture-in-Picture$"
106
-
open-floating true
107
-
}
108
-
109
-
window-rule {
110
-
match is-urgent=true
111
-
match is-floating=true
112
-
open-focused true
113
-
}
114
-
115
-
window-rule {
116
-
geometry-corner-radius 9
117
-
clip-to-geometry true
118
-
}
+164
-2
home/desktop/niri/default.nix
+164
-2
home/desktop/niri/default.nix
···
1
-
{ lib, config, pkgs, ... }:
1
+
{ lib, config, pkgs, ... } @ inputs:
2
2
3
3
with lib;
4
4
let
5
+
inherit (import ./settings.nix inputs) niri-type niri-render;
6
+
kdl = import ./kdl.nix inputs;
5
7
cfg = config.modules.desktop;
8
+
9
+
validate-niri-config = config:
10
+
pkgs.runCommand "config.kdl"
11
+
{
12
+
inherit config;
13
+
passAsFile = [ "config" ];
14
+
buildInputs = [ cfg.niri.package ];
15
+
}
16
+
''
17
+
niri validate -c $configPath
18
+
cp $configPath $out
19
+
'';
6
20
in {
7
21
options.modules.desktop.niri = {
8
22
enable = mkOption {
···
11
25
description = "Whether to enable Niri configuration.";
12
26
type = types.bool;
13
27
};
28
+
29
+
package = mkOption {
30
+
type = types.package;
31
+
default = pkgs.niri;
32
+
description = "The niri package to use.";
33
+
};
34
+
35
+
settings = mkOption {
36
+
type = types.nullOr niri-type;
37
+
default = null;
38
+
};
14
39
};
15
40
16
41
config = mkIf cfg.niri.enable {
17
-
xdg.configFile."niri/config.kdl".text = (builtins.readFile ./config.kdl);
42
+
modules.desktop.niri.settings = {
43
+
prefer-no-csd = mkDefault true;
44
+
45
+
input = {
46
+
warp-mouse-to-focus.enable = mkDefault true;
47
+
mouse = {
48
+
accel-speed = mkDefault 0.0;
49
+
accel-profile = mkDefault "flat";
50
+
};
51
+
};
52
+
53
+
cursor.hide-after-inactive-ms = mkDefault 2000;
54
+
hotkey-overlay.hide-not-bound = mkDefault true;
55
+
56
+
layout = {
57
+
gaps = mkDefault 16;
58
+
center-focused-column = mkDefault "never";
59
+
focus-ring.width = mkDefault 2;
60
+
};
61
+
62
+
window-rules = [
63
+
{
64
+
matches = [{ title = "^Picture-in-Picture$"; }];
65
+
open-floating = true;
66
+
}
67
+
{
68
+
matches = [
69
+
{ is-urgent = true; }
70
+
{ is-floating = true; }
71
+
];
72
+
open-focused = true;
73
+
}
74
+
{
75
+
geometry-corner-radius = {
76
+
top-left = 9.0;
77
+
top-right = 9.0;
78
+
bottom-left = 9.0;
79
+
bottom-right = 9.0;
80
+
};
81
+
clip-to-geometry = true;
82
+
}
83
+
];
84
+
85
+
binds = {
86
+
"Mod+T".action.spawn = "ghostty";
87
+
"Mod+Space".action.spawn = "cosmic-launcher";
88
+
"Mod+Shift+Space".action.spawn = "cosmic-app-library";
89
+
90
+
"Mod+Escape" = {
91
+
repeat = false;
92
+
action.toggle-keyboard-shortcuts-inhibit = {};
93
+
};
94
+
95
+
"Mod+O" = {
96
+
repeat = false;
97
+
action.toggle-overview = {};
98
+
};
99
+
100
+
"Mod+W" = {
101
+
repeat = false;
102
+
action.close-window = {};
103
+
};
104
+
105
+
"Mod+F".action.maximize-column = {};
106
+
"Mod+Shift+F".action.fullscreen-window = {};
107
+
"Mod+Z".action.center-column = {};
108
+
109
+
"Mod+Minus".action.set-column-width = [ "-10%" ];
110
+
"Mod+Equal".action.set-column-width = [ "+10%" ];
111
+
112
+
"Mod+Left".action.focus-column-left = {};
113
+
"Mod+Down".action.focus-window-down = {};
114
+
"Mod+Up".action.focus-window-up = {};
115
+
"Mod+Right".action.focus-column-right = {};
116
+
"Mod+H".action.focus-column-left = {};
117
+
"Mod+J".action.focus-window-or-workspace-down = {};
118
+
"Mod+K".action.focus-window-or-workspace-up = {};
119
+
"Mod+L".action.focus-column-right = {};
120
+
121
+
"Mod+Ctrl+Left".action.move-column-left = {};
122
+
"Mod+Ctrl+Down".action.move-window-down = {};
123
+
"Mod+Ctrl+Up".action.move-window-up = {};
124
+
"Mod+Ctrl+Right".action.move-column-right = {};
125
+
"Mod+Ctrl+H".action.move-column-left = {};
126
+
"Mod+Ctrl+J".action.move-window-down-or-to-workspace-down = {};
127
+
"Mod+Ctrl+K".action.move-window-up-or-to-workspace-up = {};
128
+
"Mod+Ctrl+L".action.move-column-right = {};
129
+
130
+
"Mod+Shift+Left".action.focus-monitor-left = {};
131
+
"Mod+Shift+Down".action.focus-monitor-down = {};
132
+
"Mod+Shift+Up".action.focus-monitor-up = {};
133
+
"Mod+Shift+Right".action.focus-monitor-right = {};
134
+
"Mod+Shift+H".action.focus-monitor-left = {};
135
+
"Mod+Shift+J".action.focus-monitor-down = {};
136
+
"Mod+Shift+K".action.focus-monitor-up = {};
137
+
"Mod+Shift+L".action.focus-monitor-right = {};
138
+
139
+
"Mod+Shift+Ctrl+Left".action.move-column-to-monitor-left = {};
140
+
"Mod+Shift+Ctrl+Down".action.move-column-to-monitor-down = {};
141
+
"Mod+Shift+Ctrl+Up".action.move-column-to-monitor-up = {};
142
+
"Mod+Shift+Ctrl+Right".action.move-column-to-monitor-right = {};
143
+
"Mod+Shift+Ctrl+H".action.move-column-to-monitor-left = {};
144
+
"Mod+Shift+Ctrl+J".action.move-column-to-monitor-down = {};
145
+
"Mod+Shift+Ctrl+K".action.move-column-to-monitor-up = {};
146
+
"Mod+Shift+Ctrl+L".action.move-column-to-monitor-right = {};
147
+
148
+
"Mod+Page_Down".action.focus-workspace-down = {};
149
+
"Mod+Page_Up".action.focus-workspace-up = {};
150
+
"Mod+U".action.focus-workspace-down = {};
151
+
"Mod+I".action.focus-workspace-up = {};
152
+
"Mod+Ctrl+Page_Down".action.move-column-to-workspace-down = {};
153
+
"Mod+Ctrl+Page_Up".action.move-column-to-workspace-up = {};
154
+
"Mod+Ctrl+U".action.move-column-to-workspace-down = {};
155
+
"Mod+Ctrl+I".action.move-column-to-workspace-up = {};
156
+
157
+
"Mod+Shift+Page_Down".action.move-workspace-down = {};
158
+
"Mod+Shift+Page_Up".action.move-workspace-up = {};
159
+
"Mod+Shift+U".action.move-workspace-down = {};
160
+
"Mod+Shift+I".action.move-workspace-up = {};
161
+
162
+
"Mod+WheelScrollDown".action.focus-column-right = {};
163
+
"Mod+WheelScrollUp".action.focus-column-left = {};
164
+
165
+
"Mod+BracketLeft".action.consume-or-expel-window-left = {};
166
+
"Mod+BracketRight".action.consume-or-expel-window-right = {};
167
+
"Mod+Comma".action.consume-window-into-column = {};
168
+
"Mod+Period".action.expel-window-from-column = {};
169
+
170
+
"Print".action.screenshot = {};
171
+
"Ctrl+Print".action.screenshot-screen = {};
172
+
"Alt+Print".action.screenshot-window = {};
173
+
};
174
+
};
175
+
176
+
xdg.configFile.niri-config = {
177
+
target = "niri/config.kdl";
178
+
source = validate-niri-config (kdl.serialize.nodes (niri-render cfg.niri.settings));
179
+
};
18
180
19
181
systemd.user.sessionVariables = {
20
182
XCURSOR_THEME = "Cosmic";
+390
home/desktop/niri/generate-docs.nix
+390
home/desktop/niri/generate-docs.nix
···
1
+
{ lib, ... }:
2
+
let
3
+
showOption = lib.concatStringsSep ".";
4
+
match = name: cases: cases.${name} or cases._;
5
+
indent =
6
+
entries:
7
+
"${lib.pipe entries [
8
+
lib.toList
9
+
(lib.concatStringsSep "\n")
10
+
(lib.splitString "\n")
11
+
(map (s: " ${s}"))
12
+
(lib.concatStringsSep "\n")
13
+
]}";
14
+
15
+
delimit-pretty =
16
+
start: content: end:
17
+
lib.concatStringsSep "\n" [
18
+
start
19
+
content
20
+
end
21
+
];
22
+
delimit-min =
23
+
start: content: end:
24
+
lib.concatStrings [
25
+
start
26
+
content
27
+
end
28
+
];
29
+
display-value =
30
+
{
31
+
pretty ? true,
32
+
omit-empty-composites ? false,
33
+
}:
34
+
let
35
+
display-value' = display-value { inherit pretty; };
36
+
indent' = if pretty then indent else lib.id;
37
+
delimit' = if pretty then delimit-pretty else delimit-min;
38
+
in
39
+
v:
40
+
match (builtins.typeOf v) {
41
+
string = lib.strings.escapeNixString v;
42
+
int = toString v;
43
+
float = toString v;
44
+
bool = if v then "true" else "false";
45
+
set =
46
+
if v == { } then
47
+
if omit-empty-composites then null else "{}"
48
+
else
49
+
delimit' "{" (indent' (lib.mapAttrsToList (name: val: "${name} = ${display-value' val};") v)) "}";
50
+
null = "null";
51
+
list =
52
+
if v == [ ] then
53
+
if omit-empty-composites then null else "[]"
54
+
else
55
+
delimit' "[" (indent' (map display-value' v)) "]";
56
+
_ = "<${(builtins.typeOf v)}>";
57
+
};
58
+
59
+
maybe = f: v: if v != null then f v else null;
60
+
61
+
unstable-note = ''
62
+
> [!important]
63
+
> This option is not yet available in stable niri.
64
+
>
65
+
> If you wish to modify this option, you should make sure ${link' "programs.niri.package"} is set to ${pkg-link "niri-unstable"}.
66
+
>
67
+
> Otherwise, your system might fail to build.
68
+
'';
69
+
70
+
unstable-enum = values: ''
71
+
> [!important]
72
+
> The following values for this option are not yet available in stable niri:
73
+
>
74
+
${lib.pipe values [
75
+
(map (display-value {
76
+
pretty = false;
77
+
}))
78
+
(map (s: "> - `${s}`"))
79
+
(lib.concatStringsSep "\n")
80
+
]}
81
+
>
82
+
> If you wish to use one of the mentioned values, you should make sure ${link' "programs.niri.package"} is set to ${pkg-link "niri-unstable"}.
83
+
>
84
+
> Otherwise, your system might fail to build.
85
+
'';
86
+
87
+
section =
88
+
contents:
89
+
lib.mkOption {
90
+
type = lib.mkOptionType { name = "docs-override"; };
91
+
description = contents;
92
+
};
93
+
94
+
header = title: section "# ${title}";
95
+
fake-option =
96
+
loc: contents:
97
+
section ''
98
+
## `${loc}`
99
+
100
+
${contents}
101
+
'';
102
+
103
+
link-niri-commit =
104
+
{
105
+
rev,
106
+
shortRev,
107
+
}:
108
+
"[`${shortRev}`](https://github.com/YaLTeR/niri/tree/${rev})";
109
+
link-niri-release =
110
+
version: "[`${version}`](https://github.com/YaLTeR/niri/releases/tag/${version})";
111
+
112
+
link-stylix-opt = opt: "[`${opt}`](https://danth.github.io/stylix/options/hm.html#${anchor opt})";
113
+
114
+
test = pat: str: lib.strings.match pat str != null;
115
+
116
+
anchor = lib.flip lib.pipe [
117
+
(lib.replaceStrings (lib.upperChars ++ [ " " ]) (lib.lowerChars ++ [ "-" ]))
118
+
(lib.splitString "")
119
+
(lib.filter (test "[a-z0-9-]"))
120
+
lib.concatStrings
121
+
];
122
+
anchor' = loc: anchor "`${loc}`";
123
+
124
+
link = title: "[${title}](#${anchor title})";
125
+
link' = loc: "[`${lib.removePrefix "programs.niri.settings." loc}`](#${anchor "`${loc}`"})";
126
+
127
+
module-doc =
128
+
name: desc: opts:
129
+
{
130
+
_ = section ''
131
+
# `${name}`
132
+
133
+
${desc}
134
+
'';
135
+
}
136
+
// opts;
137
+
138
+
pkg-header = name: "packages.<system>.${name}";
139
+
pkg-link = name: "[`pkgs.${name}`](#${anchor' (pkg-header name)})";
140
+
141
+
nixpkgs-link =
142
+
name: "[`pkgs.${name}`](https://search.nixos.org/packages?channel=unstable&show=${name})";
143
+
144
+
libinput-link =
145
+
page: header: "https://wayland.freedesktop.org/libinput/doc/latest/${page}.html#${anchor header}";
146
+
147
+
libinput-doc = page: header: "[${header}](${libinput-link page header})";
148
+
149
+
make-default =
150
+
text:
151
+
if lib.length (lib.splitString "\n" text) == 1 then
152
+
"- default: `${text}`"
153
+
else
154
+
''
155
+
- default:
156
+
${indent (delimit-pretty "```nix" text "```")}
157
+
'';
158
+
159
+
describe-type =
160
+
type:
161
+
let
162
+
span = content: "`${content}`";
163
+
in
164
+
if type.name == "rename" then
165
+
(span type.description) + ", which is a ${describe-type type.nestedTypes.real}"
166
+
else if type.name == "shorthand" then
167
+
link' "${type.description}"
168
+
else if type.name == "nullOr" && type.nestedTypes.elemType.name == "rename" then
169
+
span type.description
170
+
+ " (where ${span type.nestedTypes.elemType.description} is a ${describe-type type.nestedTypes.elemType.nestedTypes.real})"
171
+
else if type.name == "nullOr" && type.nestedTypes.elemType.name == "shorthand" then
172
+
span "null or" + link' "${type.nestedTypes.elemType.description}"
173
+
else
174
+
span type.description;
175
+
176
+
describe-type' = type: lib.replaceStrings [ "``" ] [ "" ] (describe-type type);
177
+
178
+
render-option =
179
+
opt:
180
+
assert opt._type or null == "option";
181
+
lib.optional (opt.visible != false) (
182
+
183
+
if opt.type.name == "docs-override" then
184
+
"${opt.description}"
185
+
else if opt.type.name == "submodule" && opt.description or null == null then
186
+
"<!-- ${showOption opt.loc} -->"
187
+
else
188
+
lib.concatStringsSep "\n" (
189
+
lib.remove null [
190
+
"## ${opt.override-header or "`${showOption opt.loc}`"}"
191
+
(
192
+
let
193
+
described = describe-type' opt.type;
194
+
in
195
+
lib.optionalString (described != "`submodule`") "- type: ${described}"
196
+
)
197
+
(maybe make-default opt.defaultText)
198
+
""
199
+
(maybe lib.id opt.description or null)
200
+
]
201
+
)
202
+
)
203
+
++ lib.optionals (opt.visible == true) (render-suboptions opt.loc (opt.type.getSubOptions opt.loc));
204
+
205
+
render-suboptions =
206
+
loc: options:
207
+
assert !(options ? _type);
208
+
let
209
+
to-list = lib.mapAttrsToList (name: opt: { inherit name opt; });
210
+
211
+
options' =
212
+
if options ? _module.niri-flake-ordered-record then
213
+
let
214
+
ord-record = options._module.niri-flake-ordered-record;
215
+
ordering = ord-record.ordering.value;
216
+
extra-docs-options = ord-record.extra-docs-options;
217
+
218
+
ordering' = builtins.listToAttrs (
219
+
lib.imap0 (i: v: {
220
+
name = v;
221
+
value = i;
222
+
}) ordering
223
+
);
224
+
max-ordering = builtins.length ordering;
225
+
in
226
+
builtins.sort (
227
+
a: b: (ordering'.${a.name} or max-ordering) < (ordering'.${b.name} or max-ordering)
228
+
) (to-list (builtins.removeAttrs (options // extra-docs-options) [ "_module" ]))
229
+
else
230
+
to-list (builtins.removeAttrs options [ "_module" ]);
231
+
232
+
in
233
+
builtins.concatMap (
234
+
{ name, opt }:
235
+
if opt ? _type then
236
+
render-option (
237
+
opt
238
+
// {
239
+
defaultText =
240
+
opt.defaultText
241
+
or (if opt ? default then display-value { omit-empty-composites = true; } opt.default else null);
242
+
visible = if opt.niri-flake-document-internal or false then true else opt.visible or true;
243
+
loc = opt.override-loc or lib.id opt.loc;
244
+
}
245
+
)
246
+
else
247
+
render-suboptions (loc ++ [ name ]) opt
248
+
) options';
249
+
250
+
make-docs = lib.flip lib.pipe [
251
+
lib.types.submodule
252
+
(m: m.getSubOptions [ ])
253
+
(render-suboptions [ ])
254
+
(lib.concatStringsSep "\n\n")
255
+
];
256
+
in
257
+
{
258
+
inherit make-docs;
259
+
lib = {
260
+
inherit
261
+
unstable-note
262
+
unstable-enum
263
+
section
264
+
header
265
+
fake-option
266
+
test
267
+
anchor
268
+
anchor'
269
+
link
270
+
link'
271
+
module-doc
272
+
pkg-header
273
+
pkg-link
274
+
nixpkgs-link
275
+
libinput-link
276
+
libinput-doc
277
+
link-niri-commit
278
+
link-niri-release
279
+
link-stylix-opt
280
+
link-this-github
281
+
display-value
282
+
;
283
+
};
284
+
285
+
settings-fmt =
286
+
let
287
+
body = lib.strings.trimWith { end = true; };
288
+
289
+
indent-except-first-line =
290
+
text:
291
+
let
292
+
lines = lib.splitString "\n" text;
293
+
lines' = [ (builtins.head lines) ] ++ (map (line: " ${line}") (builtins.tail lines));
294
+
in
295
+
296
+
if lines == [ ] then "" else builtins.concatStringsSep "\n" lines';
297
+
in
298
+
rec {
299
+
link-to-setting = loc: "#${anchor' "`${showOption loc}`"}";
300
+
301
+
bare-link = url: url;
302
+
masked-link =
303
+
{
304
+
href,
305
+
content,
306
+
}:
307
+
"[${content}](${href})";
308
+
309
+
block-quote =
310
+
content:
311
+
lib.pipe content [
312
+
body
313
+
(lib.splitString "\n")
314
+
(map (s: "> ${s}\n"))
315
+
lib.concatStrings
316
+
];
317
+
318
+
code = code: "`${code}`";
319
+
320
+
admonition = lib.genAttrs [ "note" "tip" "important" "warning" "caution" ] (
321
+
kind: content:
322
+
block-quote ''
323
+
[!${kind}]
324
+
${content}
325
+
''
326
+
);
327
+
328
+
list = items: ''
329
+
${lib.concatStringsSep "\n" (map (item: "- ${indent-except-first-line (body item)}") items)}
330
+
'';
331
+
ordered-list = items: ''
332
+
${lib.concatStringsSep "\n" (map (item: "1. ${indent-except-first-line (body item)}") items)}
333
+
'';
334
+
335
+
nix-code-block = code: ''
336
+
```nix
337
+
${body code}
338
+
```
339
+
'';
340
+
341
+
em = text: "*${text}*";
342
+
strong = text: "**${text}**";
343
+
344
+
table =
345
+
{
346
+
headers,
347
+
align,
348
+
rows,
349
+
}:
350
+
assert (builtins.length headers == builtins.length align);
351
+
''
352
+
| ${builtins.concatStringsSep " | " headers} |
353
+
| ${
354
+
builtins.concatStringsSep " | " (
355
+
map (
356
+
align:
357
+
if align == null then
358
+
"---"
359
+
else
360
+
{
361
+
left = ":---";
362
+
center = ":---:";
363
+
right = "---:";
364
+
}
365
+
.${align}
366
+
) align
367
+
)
368
+
} |
369
+
${lib.concatStringsSep "\n" (
370
+
map (
371
+
row:
372
+
assert builtins.length headers == builtins.length row;
373
+
"| ${builtins.concatStringsSep " | " row} |"
374
+
) rows
375
+
)}
376
+
'';
377
+
378
+
kbd = code;
379
+
380
+
img =
381
+
{
382
+
src,
383
+
title,
384
+
alt,
385
+
}:
386
+
''
387
+

388
+
'';
389
+
};
390
+
}
+224
home/desktop/niri/kdl.nix
+224
home/desktop/niri/kdl.nix
···
1
+
{ lib, ... }:
2
+
let
3
+
fold-args =
4
+
lib.foldl
5
+
(
6
+
self: arg:
7
+
if lib.isAttrs arg then
8
+
self // { properties = self.properties // arg; }
9
+
else
10
+
self // { arguments = self.arguments ++ [ arg ]; }
11
+
)
12
+
{
13
+
arguments = [ ];
14
+
properties = { };
15
+
};
16
+
node = name: args: children: {
17
+
inherit name;
18
+
inherit (fold-args (lib.toList args)) arguments properties;
19
+
inherit children;
20
+
};
21
+
22
+
plain = name: node name [ ];
23
+
leaf = name: args: node name args [ ];
24
+
magic-leaf = node-name: {
25
+
${node-name} = [ ];
26
+
__functor = self: arg: {
27
+
inherit (self) __functor;
28
+
${node-name} = self.${node-name} ++ lib.toList arg;
29
+
};
30
+
};
31
+
flag = name: node name [ ] [ ];
32
+
33
+
serialize.string = lib.flip lib.pipe [
34
+
(lib.escape [
35
+
"\\"
36
+
"\""
37
+
])
38
+
# including newlines will cause the serialized output to contain additional indentation
39
+
# so we escape them
40
+
(lib.replaceStrings [ "\n" ] [ "\\n" ])
41
+
(v: "\"${v}\"")
42
+
];
43
+
serialize.path = serialize.string;
44
+
serialize.int = toString;
45
+
serialize.float = toString;
46
+
serialize.bool = v: if v then "true" else "false";
47
+
serialize.null = lib.const "null";
48
+
49
+
serialize.value = v: serialize.${builtins.typeOf v} v;
50
+
51
+
# this is not a complete list of valid identifiers
52
+
# but it is good enough for niri
53
+
# if this rejects a valid ident, literally nothing bad happens
54
+
# essentially, this regex boils down to any sequence of letters, numbers or +/-
55
+
# but not something that looks like a number (e.g. 0, -4, +12)
56
+
bare-ident = "[A-Za-z][A-Za-z0-9+-]*|[+-]|[+-][A-Za-z+-][A-Za-z0-9+-]*";
57
+
serialize.ident = v: if lib.strings.match bare-ident v != null then v else serialize.string v;
58
+
59
+
serialize.prop =
60
+
{
61
+
name,
62
+
value,
63
+
}:
64
+
"${serialize.ident name}=${serialize.value value}";
65
+
66
+
single-indent = " ";
67
+
68
+
should-collapse =
69
+
children:
70
+
let
71
+
length = lib.length children;
72
+
in
73
+
length == 0 || (length == 1 && should-collapse (lib.head children).children);
74
+
75
+
serialize.node = serialize.node-with "";
76
+
serialize.node-with =
77
+
indent:
78
+
{
79
+
name,
80
+
arguments,
81
+
properties,
82
+
children,
83
+
}:
84
+
indent
85
+
+ lib.concatStringsSep " " (
86
+
lib.flatten [
87
+
(serialize.ident name)
88
+
(map serialize.value arguments)
89
+
(map serialize.prop (lib.attrsToList properties))
90
+
(
91
+
if lib.length children == 0 then
92
+
[ ]
93
+
else if should-collapse children then
94
+
"{ ${serialize.nodes children}; }"
95
+
else
96
+
"{\n${serialize.nodes-with (indent + single-indent) children}\n${indent}}"
97
+
)
98
+
]
99
+
);
100
+
101
+
serialize.nodes = serialize.nodes-with "";
102
+
serialize.nodes-with =
103
+
indent:
104
+
lib.flip lib.pipe [
105
+
(map (serialize.node-with indent))
106
+
(lib.concatStringsSep "\n")
107
+
];
108
+
109
+
kdl-value = lib.types.nullOr (
110
+
lib.types.oneOf [
111
+
lib.types.str
112
+
lib.types.int
113
+
lib.types.float
114
+
lib.types.bool
115
+
]
116
+
);
117
+
118
+
kdl-node = lib.types.submodule {
119
+
options.name = lib.mkOption {
120
+
type = lib.types.str;
121
+
};
122
+
options.arguments = lib.mkOption {
123
+
type = lib.types.listOf kdl-value;
124
+
default = [ ];
125
+
};
126
+
options.properties = lib.mkOption {
127
+
type = lib.types.attrsOf kdl-value;
128
+
default = { };
129
+
};
130
+
options.children = lib.mkOption {
131
+
type = kdl-document;
132
+
default = [ ];
133
+
};
134
+
};
135
+
136
+
kdl-leaf = lib.mkOptionType {
137
+
name = "kdl-leaf";
138
+
description = "kdl leaf";
139
+
descriptionClass = "noun";
140
+
check =
141
+
v: lib.isAttrs v && lib.length (builtins.attrNames (builtins.removeAttrs v [ "__functor" ])) == 1;
142
+
merge = lib.mergeUniqueOption {
143
+
message = "";
144
+
merge =
145
+
loc: defs:
146
+
let
147
+
def = builtins.head defs;
148
+
149
+
name = builtins.head (builtins.attrNames (builtins.removeAttrs def.value [ "__functor" ]));
150
+
151
+
args = kdl-args.merge (loc ++ name) [
152
+
{
153
+
inherit (def) file;
154
+
value = def.value.${name};
155
+
}
156
+
];
157
+
in
158
+
{
159
+
${name} = args;
160
+
};
161
+
};
162
+
};
163
+
164
+
kdl-args =
165
+
let
166
+
arg = lib.types.either (lib.types.attrsOf kdl-value) kdl-value;
167
+
args = lib.types.either (lib.types.listOf arg) arg;
168
+
in
169
+
lib.mkOptionType {
170
+
name = "kdl-args";
171
+
description = "kdl arguments";
172
+
descriptionClass = "noun";
173
+
174
+
inherit (lib.types.uniq args) check merge;
175
+
};
176
+
177
+
kdl-nodes = lib.types.listOf kdl-node // {
178
+
name = "kdl-nodes";
179
+
description = "kdl nodes";
180
+
descriptionClass = "noun";
181
+
};
182
+
183
+
kdl-document = lib.mkOptionType {
184
+
name = "kdl-document";
185
+
description = "kdl document";
186
+
descriptionClass = "noun";
187
+
188
+
check = v: builtins.isList v || builtins.isAttrs v;
189
+
merge =
190
+
loc: defs:
191
+
kdl-nodes.merge loc (
192
+
map (def: {
193
+
inherit (def) file;
194
+
value =
195
+
let
196
+
value' = lib.remove null (lib.flatten def.value);
197
+
in
198
+
lib.warnIf (def.value != value')
199
+
"kdl document defined in `${def.file}` for `${lib.showOption loc}` is not normalized. please ensure that it is a flat list of nodes."
200
+
value';
201
+
}) defs
202
+
);
203
+
};
204
+
in
205
+
{
206
+
inherit
207
+
node
208
+
plain
209
+
leaf
210
+
magic-leaf
211
+
flag
212
+
serialize
213
+
;
214
+
types = {
215
+
inherit
216
+
kdl-value
217
+
kdl-node
218
+
kdl-nodes
219
+
kdl-leaf
220
+
kdl-args
221
+
kdl-document
222
+
;
223
+
};
224
+
}
+3367
home/desktop/niri/settings.nix
+3367
home/desktop/niri/settings.nix
···
1
+
{ lib, ... } @ inputs:
2
+
3
+
let
4
+
kdl = import ./kdl.nix inputs;
5
+
docs = import ./docs.nix inputs;
6
+
7
+
type-with =
8
+
fmt:
9
+
let
10
+
inherit (lib)
11
+
flip
12
+
pipe
13
+
showOption
14
+
mkOption
15
+
mkOptionType
16
+
types
17
+
;
18
+
inherit (lib.types)
19
+
nullOr
20
+
attrsOf
21
+
listOf
22
+
submodule
23
+
enum
24
+
;
25
+
26
+
record = record' null;
27
+
28
+
record' =
29
+
description: options:
30
+
types.submoduleWith {
31
+
inherit description;
32
+
shorthandOnlyDefinesConfig = true;
33
+
modules = [
34
+
{ inherit options; }
35
+
];
36
+
};
37
+
38
+
required = type: mkOption { inherit type; };
39
+
nullable = type: optional (nullOr type) null;
40
+
optional = type: default: mkOption { inherit type default; };
41
+
readonly = type: value: optional type value // { readOnly = true; };
42
+
docs-only =
43
+
type:
44
+
required (type // { check = _: true; })
45
+
// {
46
+
internal = true;
47
+
visible = false;
48
+
readOnly = true;
49
+
apply = _: null;
50
+
niri-flake-document-internal = true;
51
+
};
52
+
53
+
attrs = type: optional (attrsOf type) { };
54
+
list = type: optional (listOf type) [ ];
55
+
56
+
attrs-record = attrs-record' null;
57
+
58
+
attrs-record' =
59
+
description: opts:
60
+
attrs (
61
+
if builtins.isFunction opts then
62
+
types.submoduleWith {
63
+
inherit description;
64
+
shorthandOnlyDefinesConfig = true;
65
+
modules = [
66
+
(
67
+
{ name, ... }:
68
+
{
69
+
options = opts name;
70
+
}
71
+
)
72
+
];
73
+
}
74
+
else
75
+
record' description opts
76
+
);
77
+
78
+
float-or-int = types.either types.float types.int;
79
+
80
+
obsolete-warning = from: to: defs: ''
81
+
${from} is obsolete.
82
+
Use ${to} instead.
83
+
${builtins.concatStringsSep "\n" (map (def: "- defined in ${def.file}") defs)}
84
+
'';
85
+
86
+
rename-warning = from: to: obsolete-warning (showOption from) (showOption to);
87
+
88
+
libinput-anchor-for-header = lib.flip lib.pipe [
89
+
(lib.replaceStrings (lib.upperChars ++ [ " " ]) (lib.lowerChars ++ [ "-" ]))
90
+
(lib.splitString "")
91
+
(lib.filter (str: lib.strings.match "[a-z0-9-]" str != null))
92
+
lib.concatStrings
93
+
];
94
+
libinput-link-href =
95
+
page: header:
96
+
"https://wayland.freedesktop.org/libinput/doc/latest/${page}.html#${libinput-anchor-for-header header}";
97
+
libinput-link = page: header: fmt.bare-link (libinput-link-href page header);
98
+
99
+
libinput-doc =
100
+
page: header:
101
+
fmt.masked-link {
102
+
href = libinput-link-href page header;
103
+
content = header;
104
+
};
105
+
106
+
link-niri-release =
107
+
version:
108
+
fmt.masked-link {
109
+
href = "https://github.com/YaLTeR/niri/releases/tag/${version}";
110
+
content = fmt.code version;
111
+
};
112
+
113
+
link' =
114
+
loc:
115
+
fmt.masked-link {
116
+
href = fmt.link-to-setting loc;
117
+
content = fmt.code (lib.removePrefix "programs.niri.settings." (lib.showOption loc));
118
+
};
119
+
120
+
subopts =
121
+
opt:
122
+
assert opt._type == "option";
123
+
opt.type.getSubOptions opt.loc;
124
+
link-opt =
125
+
opt:
126
+
assert opt._type == "option";
127
+
link' opt.loc;
128
+
129
+
unstable-note = fmt.admonition.important ''
130
+
This option is not yet available in stable niri.
131
+
132
+
If you wish to modify this option, you should make sure you're using the latest unstable niri.
133
+
134
+
Otherwise, your system might fail to build.
135
+
'';
136
+
137
+
basic-pointer = default-natural-scroll: {
138
+
natural-scroll = optional types.bool default-natural-scroll // {
139
+
description = ''
140
+
Whether scrolling should move the content in the scrolled direction (as opposed to moving the viewport)
141
+
142
+
Further reading:
143
+
${fmt.list [
144
+
(libinput-link "configuration" "Scrolling")
145
+
(libinput-link "scrolling" "Natural scrolling vs. traditional scrolling")
146
+
]}
147
+
'';
148
+
};
149
+
middle-emulation = optional types.bool false // {
150
+
description = ''
151
+
Whether a middle mouse button press should be sent when you press the left and right mouse buttons
152
+
153
+
Further reading:
154
+
${fmt.list [
155
+
(libinput-link "configuration" "Middle Button Emulation")
156
+
(libinput-link "middle-button-emulation" "Middle button emulation")
157
+
]}
158
+
'';
159
+
};
160
+
accel-speed = nullable float-or-int // {
161
+
description = ''
162
+
Further reading:
163
+
${fmt.list [
164
+
(libinput-link "configuration" "Pointer acceleration")
165
+
]}
166
+
'';
167
+
};
168
+
accel-profile =
169
+
nullable (enum [
170
+
"adaptive"
171
+
"flat"
172
+
])
173
+
// {
174
+
description = ''
175
+
Further reading:
176
+
${fmt.list [
177
+
(libinput-link "pointer-acceleration" "Pointer acceleration profiles")
178
+
]}
179
+
'';
180
+
};
181
+
scroll-button = nullable types.int // {
182
+
description =
183
+
let
184
+
input-event-codes-h = fmt.masked-link {
185
+
href = "https://github.com/torvalds/linux/blob/e42b1a9a2557aa94fee47f078633677198386a52/include/uapi/linux/input-event-codes.h#L355-L363";
186
+
content = fmt.code "input-event-codes.h";
187
+
};
188
+
in
189
+
''
190
+
When ${fmt.code ''scroll-method = "on-button-down"''}, this is the button that will be used to enable scrolling. This button must be on the same physical device as the pointer, according to libinput docs. The type is a button code, as defined in ${input-event-codes-h}. Most commonly, this will be set to ${fmt.code "BTN_LEFT"}, ${fmt.code "BTN_MIDDLE"}, or ${fmt.code "BTN_RIGHT"}, or at least some mouse button, but any button from that file is a valid value for this option (though, libinput may not necessarily do anything useful with most of them)
191
+
192
+
Further reading:
193
+
${fmt.list [
194
+
(libinput-link "scrolling" "On-Button scrolling")
195
+
]}
196
+
'';
197
+
};
198
+
scroll-button-lock = optional types.bool false // {
199
+
description = ''
200
+
When this is false, ${fmt.code "scroll-button"} needs to be held down for pointer motion to be converted to scrolling. When this is true, ${fmt.code "scroll-button"} can be pressed and released to "lock" the device into this state, until it is pressed and released a second time.
201
+
202
+
Further reading:
203
+
${fmt.list [
204
+
(libinput-link "scrolling" "On-Button scrolling")
205
+
]}
206
+
'';
207
+
};
208
+
scroll-method =
209
+
nullable (
210
+
types.enum [
211
+
"no-scroll"
212
+
"two-finger"
213
+
"edge"
214
+
"on-button-down"
215
+
]
216
+
)
217
+
// {
218
+
description = ''
219
+
When to convert motion events to scrolling events.
220
+
The default and supported values vary based on the device type.
221
+
222
+
Further reading:
223
+
${fmt.list [
224
+
(libinput-link "scrolling" "Scrolling")
225
+
]}
226
+
'';
227
+
};
228
+
};
229
+
230
+
pointer-tablet-common = {
231
+
enable = optional types.bool true;
232
+
left-handed = optional types.bool false // {
233
+
description = ''
234
+
Whether to accomodate left-handed usage for this device.
235
+
This varies based on the exact device, but will for example swap left/right mouse buttons.
236
+
237
+
Further reading:
238
+
${fmt.list [
239
+
(libinput-link "configuration" "Left-handed Mode")
240
+
]}
241
+
'';
242
+
};
243
+
};
244
+
245
+
preset-size =
246
+
dimension: object:
247
+
types.attrTag {
248
+
fixed = lib.mkOption {
249
+
type = types.int;
250
+
description = ''
251
+
The ${dimension} of the ${object} in logical pixels
252
+
'';
253
+
};
254
+
proportion = lib.mkOption {
255
+
type = types.float;
256
+
description = ''
257
+
The ${dimension} of the ${object} as a proportion of the screen's ${dimension}
258
+
'';
259
+
};
260
+
};
261
+
262
+
preset-width = preset-size "width" "column";
263
+
preset-height = preset-size "height" "window";
264
+
265
+
emptyOr =
266
+
elemType:
267
+
mkOptionType {
268
+
name = "emptyOr";
269
+
description =
270
+
if
271
+
builtins.elem elemType.descriptionClass [
272
+
"noun"
273
+
"conjunction"
274
+
]
275
+
then
276
+
"{} or ${elemType.description}"
277
+
else
278
+
"{} or (${elemType.description})";
279
+
descriptionClass = "conjunction";
280
+
check = v: v == { } || elemType.check v;
281
+
nestedTypes.elemType = elemType;
282
+
merge =
283
+
loc: defs: if builtins.all (def: def.value == { }) defs then { } else elemType.merge loc defs;
284
+
285
+
inherit (elemType) getSubOptions;
286
+
};
287
+
288
+
default-width = emptyOr preset-width;
289
+
default-height = emptyOr preset-height;
290
+
291
+
shorthand-for =
292
+
type-name: real:
293
+
mkOptionType {
294
+
name = "shorthand";
295
+
description = "<${type-name}>";
296
+
descriptionClass = "noun";
297
+
inherit (real) check merge getSubOptions;
298
+
nestedTypes = { inherit real; };
299
+
};
300
+
301
+
rename =
302
+
name: real:
303
+
mkOptionType {
304
+
name = "rename";
305
+
description = "${name}";
306
+
descriptionClass = "noun";
307
+
inherit (real) check merge getSubOptions;
308
+
nestedTypes = { inherit real; };
309
+
};
310
+
311
+
# niri seems to have deprecated this way of defining colors; so we won't support it
312
+
# color-array = mkOptionType {
313
+
# name = "color";
314
+
# description = "[red green blue alpha]";
315
+
# descriptionClass = "noun";
316
+
# check = v: isList v && length v == 4 && all isInt v;
317
+
# };
318
+
319
+
decoration =
320
+
self:
321
+
322
+
let
323
+
css-color = fmt.masked-link {
324
+
href = "https://developer.mozilla.org/en-US/docs/Web/CSS/color_value";
325
+
content = fmt.code "<color>";
326
+
};
327
+
328
+
css-linear-gradient = fmt.masked-link {
329
+
href = "https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient";
330
+
content = fmt.code "linear-gradient()";
331
+
};
332
+
333
+
css-color-interpolation-method = fmt.masked-link {
334
+
href = "https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method";
335
+
content = fmt.code "<color-interpolation-method>";
336
+
};
337
+
338
+
csscolorparser-crate = fmt.masked-link {
339
+
href = "https://crates.io/crates/csscolorparser";
340
+
content = fmt.code "csscolorparser";
341
+
};
342
+
in
343
+
types.attrTag {
344
+
color = lib.mkOption {
345
+
type = types.str;
346
+
description = ''
347
+
A solid color to use for the decoration.
348
+
349
+
This is a CSS ${css-color} value, like ${fmt.code ''"rgb(255 0 0)"''}, ${fmt.code ''"#C0FFEE"''}, or ${fmt.code ''"sandybrown"''}.
350
+
351
+
The specific crate that niri uses to parse this also supports some nonstandard color functions, like ${fmt.code "hwba()"}, ${fmt.code "hsv()"}, ${fmt.code "hsva()"}. See ${csscolorparser-crate} for details.
352
+
'';
353
+
};
354
+
gradient = lib.mkOption {
355
+
description = ''
356
+
A linear gradient to use for the decoration.
357
+
358
+
This is meant to approximate the CSS ${css-linear-gradient} function, but niri does not fully support all the same parameters. Only an angle in degrees is supported.
359
+
'';
360
+
type = record' "gradient" {
361
+
from = required types.str // {
362
+
description = ''
363
+
The starting ${css-color} of the gradient.
364
+
365
+
For more details, see ${link-opt (subopts self).color}.
366
+
'';
367
+
};
368
+
to = required types.str // {
369
+
description = ''
370
+
The ending ${css-color} of the gradient.
371
+
372
+
For more details, see ${link-opt (subopts self).color}.
373
+
'';
374
+
};
375
+
angle = optional types.int 180 // {
376
+
description = ''
377
+
The angle of the gradient, in degrees, measured clockwise from a gradient that starts at the bottom and ends at the top.
378
+
379
+
This is the same as the angle parameter in the CSS ${css-linear-gradient} function, except you can only express it in degrees.
380
+
'';
381
+
};
382
+
in' =
383
+
nullable (enum [
384
+
"srgb"
385
+
"srgb-linear"
386
+
"oklab"
387
+
"oklch shorter hue"
388
+
"oklch longer hue"
389
+
"oklch increasing hue"
390
+
"oklch decreasing hue"
391
+
])
392
+
// {
393
+
description = ''
394
+
The colorspace to interpolate the gradient in. This option is named ${fmt.code "in'"} because ${fmt.code "in"} is a reserved keyword in Nix.
395
+
396
+
This is a subset of the ${css-color-interpolation-method} values in CSS.
397
+
'';
398
+
};
399
+
relative-to =
400
+
optional (enum [
401
+
"window"
402
+
"workspace-view"
403
+
]) "window"
404
+
// {
405
+
description = ''
406
+
The rectangle that this gradient is contained within.
407
+
408
+
If a gradient is ${fmt.code "relative-to"} the ${fmt.code ''"window"''}, then the gradient will start and stop at the window bounds. If you have many windows, then the gradients will have many starts and stops.
409
+
410
+
${fmt.img {
411
+
src = "/assets/relative-to-window.png";
412
+
alt = ''
413
+
four windows arranged in two columns; a big window to the left of three stacked windows.
414
+
a gradient is drawn from the bottom left corner of each window, which is yellow, transitioning to red at the top right corner of each window.
415
+
the three vertical windows look identical, with a yellow and red corner, and the other two corners are slightly different shades of orange.
416
+
the big window has a yellow and red corner, with the top left corner being a very red orange orange, and the bottom right corner being a very yellow orange.
417
+
the top edge of the top stacked window has a noticeable transition from a yellowish orange to completely red.
418
+
'';
419
+
title = ''behaviour of relative-to="window"'';
420
+
}}
421
+
422
+
If the gradient is instead ${fmt.code "relative-to"} the ${fmt.code ''"workspace-view"''}, then the gradient will start and stop at the bounds of your view. Windows decorations will take on the color values from just the part of the screen that they occupy
423
+
424
+
${fmt.img {
425
+
src = "/assets/relative-to-workspace-view.png";
426
+
alt = ''
427
+
four windows arranged in two columns; a big window to the left of three stacked windows.
428
+
a gradient is drawn from the bottom left corner of the workspace view, which is yellow, transitioning to red at the top right corner of the workspace view.
429
+
it looks like the gradient starts in the bottom left of the big window, and ends in the top right of the upper stacked window.
430
+
the bottom left corner of the top stacked window is a red orange color, and the bottom left corner of the middle stacked window is a more neutral orange color.
431
+
the bottom edge of the big window is almost entirely yellow, and the top edge of the top stacked window is almost entirely red.
432
+
'';
433
+
title = ''behaviour of relative-to="workspace-view"'';
434
+
}}
435
+
436
+
these beautiful images are sourced from the release notes for ${link-niri-release "v0.1.3"}
437
+
'';
438
+
};
439
+
};
440
+
};
441
+
};
442
+
443
+
make-decoration-options =
444
+
options:
445
+
builtins.mapAttrs (
446
+
name:
447
+
{ description }:
448
+
nullable (shorthand-for "decoration" (decoration (options.${name})))
449
+
// {
450
+
visible = "shallow";
451
+
inherit description;
452
+
}
453
+
);
454
+
455
+
borderish =
456
+
{
457
+
enable-by-default,
458
+
name,
459
+
window,
460
+
description,
461
+
}:
462
+
section' (
463
+
{ options, ... }:
464
+
{
465
+
imports = make-ordered-options [
466
+
{
467
+
enable = optional types.bool enable-by-default // {
468
+
description = ''
469
+
Whether to enable the ${name}.
470
+
'';
471
+
};
472
+
width = optional float-or-int 4 // {
473
+
description = ''
474
+
The width of the ${name} drawn around each ${window}.
475
+
'';
476
+
};
477
+
}
478
+
479
+
(make-decoration-options options {
480
+
urgent.description = ''
481
+
The color of the ${name} for windows that are requesting attention.
482
+
'';
483
+
active.description = ''
484
+
The color of the ${name} for the window that has keyboard focus.
485
+
'';
486
+
inactive.description = ''
487
+
The color of the ${name} for windows that do not have keyboard focus.
488
+
'';
489
+
})
490
+
];
491
+
}
492
+
)
493
+
// {
494
+
inherit description;
495
+
};
496
+
497
+
border-rule =
498
+
{
499
+
name,
500
+
description,
501
+
window,
502
+
}:
503
+
section' (
504
+
{ options, ... }:
505
+
{
506
+
imports = make-ordered-options [
507
+
{
508
+
enable = nullable types.bool // {
509
+
description = ''
510
+
Whether to enable the ${name}.
511
+
'';
512
+
};
513
+
width = nullable float-or-int // {
514
+
description = ''
515
+
The width of the ${name} drawn around each ${window}.
516
+
'';
517
+
};
518
+
}
519
+
520
+
(make-decoration-options options {
521
+
urgent.description = ''
522
+
The color of the ${name} for windows that are requesting attention.
523
+
'';
524
+
active.description = ''
525
+
The color of the ${name} for the window that has keyboard focus.
526
+
'';
527
+
inactive.description = ''
528
+
The color of the ${name} for windows that do not have keyboard focus.
529
+
'';
530
+
})
531
+
];
532
+
}
533
+
)
534
+
// {
535
+
inherit description;
536
+
};
537
+
538
+
shadow-rule = section {
539
+
enable = nullable types.bool;
540
+
offset =
541
+
nullable (record {
542
+
x = required float-or-int;
543
+
y = required float-or-int;
544
+
})
545
+
// {
546
+
description = shadow-descriptions.offset;
547
+
};
548
+
549
+
softness = nullable float-or-int // {
550
+
description = shadow-descriptions.softness;
551
+
};
552
+
553
+
spread = nullable float-or-int // {
554
+
description = shadow-descriptions.spread;
555
+
};
556
+
557
+
draw-behind-window = nullable types.bool;
558
+
559
+
color = nullable types.str;
560
+
561
+
inactive-color = nullable types.str;
562
+
};
563
+
564
+
geometry-corner-radius-rule = nullable (record {
565
+
top-left = required types.float;
566
+
top-right = required types.float;
567
+
bottom-right = required types.float;
568
+
bottom-left = required types.float;
569
+
});
570
+
571
+
shadow-descriptions =
572
+
let
573
+
css-box-shadow =
574
+
prop:
575
+
fmt.masked-link {
576
+
href = "https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#syntax";
577
+
content = "CSS box-shadow ${prop}";
578
+
};
579
+
in
580
+
{
581
+
offset = ''
582
+
The offset of the shadow from the window, measured in logical pixels.
583
+
584
+
This behaves like a ${css-box-shadow "offset"}
585
+
'';
586
+
587
+
softness = ''
588
+
The softness/size of the shadow, measured in logical pixels.
589
+
590
+
This behaves like a ${css-box-shadow "blur radius"}
591
+
'';
592
+
593
+
spread = ''
594
+
The spread of the shadow, measured in logical pixels.
595
+
596
+
This behaves like a ${css-box-shadow "spread radius"}
597
+
'';
598
+
};
599
+
600
+
regex = rename "regular expression" types.str;
601
+
602
+
rule-descriptions =
603
+
{
604
+
surface,
605
+
surfaces,
606
+
surface-rule,
607
+
Surface-rules,
608
+
example-fields,
609
+
610
+
self,
611
+
spawn-at-startup,
612
+
}:
613
+
614
+
let
615
+
matches = link-opt (subopts self).matches;
616
+
excludes = link-opt (subopts self).excludes;
617
+
in
618
+
{
619
+
top-option = ''
620
+
${Surface-rules}.
621
+
622
+
A ${surface-rule} will match based on ${matches} and ${excludes}. Both of these are lists of "match rules".
623
+
624
+
A given match rule can match based on one of several fields. For a given match rule to "match" a ${surface}, it must match on all fields.
625
+
626
+
${fmt.list (
627
+
example-fields
628
+
++ [
629
+
"The ${fmt.code "at-startup"} field, when non-null, will match a ${surface} based on whether it was opened within the first 60 seconds of niri starting up."
630
+
"If a field is null, it will always match."
631
+
]
632
+
)}
633
+
634
+
For a given ${surface-rule} to match a ${surface}, the above logic is employed to determine whether any given match rule matches, and the interactions between the match rules decide whether the ${surface-rule} as a whole will match. For a given ${surface-rule}:
635
+
636
+
${fmt.list [
637
+
''
638
+
A given ${surface} is "considered" if any of the match rules in ${matches} successfully match this ${surface}. If all of the match rules do not match this ${surface}, then that ${surface} will never match this ${surface-rule}.
639
+
''
640
+
''
641
+
If ${matches} contains no match rules, it will match any ${surface} and "consider" it for this ${surface-rule}.
642
+
''
643
+
''
644
+
If a given ${surface} is "considered" for this ${surface-rule} according to the above rules, the selection can be further refined with ${excludes}. If any of the match rules in ${fmt.code "excludes"} match this ${surface}, it will be rejected and this ${surface-rule} will not match the given ${surface}.
645
+
''
646
+
]}
647
+
648
+
That is, a given ${surface-rule} will apply to a given ${surface} if any of the entries in ${matches} match that ${surface} (or there are none), AND none of the entries in ${excludes} match that ${surface}.
649
+
650
+
All fields of a ${surface-rule} can be set to null, which represents that the field shall have no effect on the ${surface} (and in general, the client is allowed to choose the initial value).
651
+
652
+
To compute the final set of ${surface-rule}s that apply to a given ${surface}, each ${surface-rule} in this list is consdered in order.
653
+
654
+
At first, every field is set to null.
655
+
656
+
Then, for each applicable ${surface-rule}:
657
+
658
+
${fmt.list [
659
+
''
660
+
If a given field is null on this ${surface-rule}, it has no effect. It does nothing and "inherits" the value from the previous rule.
661
+
''
662
+
''
663
+
If the given field is not null, it will overwrite the value from any previous rule.
664
+
''
665
+
]}
666
+
667
+
The "final value" of a field is simply its value at the end of this process. That is, the final value of a field is the one from the ${fmt.em "last"} ${surface-rule} that matches the given ${surface-rule} (not considering null entries, unless there are no non-null entries)
668
+
669
+
If the final value of a given field is null, then it usually means that the client gets to decide. For more information, see the documentation for each field.
670
+
'';
671
+
672
+
match = ''
673
+
A list of rules to match ${surfaces}.
674
+
675
+
If any of these rules match a ${surface} (or there are none), that ${surface-rule} will be considered for this ${surface}. It can still be rejected by ${excludes}
676
+
677
+
If all of the rules do not match a ${surface}, then this ${surface-rule} will not apply to that ${surface}.
678
+
'';
679
+
680
+
exclude = ''
681
+
A list of rules to exclude ${surfaces}.
682
+
683
+
If any of these rules match a ${surface}, then this ${surface-rule} will not apply to that ${surface}, even if it matches one of the rules in ${matches}
684
+
685
+
If none of these rules match a ${surface}, then this ${surface-rule} will not be rejected. It will apply to that ${surface} if and only if it matches one of the rules in ${matches}
686
+
'';
687
+
688
+
match-at-startup = ''
689
+
When true, this rule will match ${surfaces} opened within the first 60 seconds of niri starting up. When false, this rule will match ${surfaces} opened ${fmt.em "more than"} 60 seconds after niri started up. This is useful for applying different rules to ${surfaces} opened from ${link-opt spawn-at-startup} versus those opened later.
690
+
'';
691
+
692
+
opacity = ''
693
+
The opacity of the ${surface}, ranging from 0 to 1.
694
+
695
+
If the final value of this field is null, niri will fall back to a value of 1.
696
+
697
+
Note that this is applied in addition to the opacity set by the client. Setting this to a semitransparent value on a ${surface} that is already semitransparent will make it even more transparent.
698
+
'';
699
+
700
+
block-out-from = ''
701
+
Whether to block out this ${surface} from screen captures. When the final value of this field is null, it is not blocked out from screen captures.
702
+
703
+
This is useful to protect sensitive information, like the contents of password managers or private chats. It is very important to understand the implications of this option, as described below, ${fmt.strong "especially if you are a streamer or content creator"}.
704
+
705
+
Some of this may be obvious, but in general, these invariants ${fmt.em "should"} hold true:
706
+
${fmt.list [
707
+
''
708
+
a ${surface} is never meant to be blocked out from the actual physical screen (otherwise you wouldn't be able to see it at all)
709
+
''
710
+
''
711
+
a ${fmt.code "block-out-from"} ${surface} ${fmt.em "is"} meant to be always blocked out from screencasts (as they are often used for livestreaming etc)
712
+
''
713
+
''
714
+
a ${fmt.code "block-out-from"} ${surface} is ${fmt.em "not"} supposed to be blocked from screenshots (because usually these are not broadcasted live, and you generally know what you're taking a screenshot of)
715
+
''
716
+
]}
717
+
718
+
There are three methods of screencapture in niri:
719
+
720
+
${fmt.ordered-list [
721
+
''
722
+
The ${fmt.code "org.freedesktop.portal.ScreenCast"} interface, which is used by tools like OBS primarily to capture video. When ${fmt.code ''block-out-from = "screencast";''} or ${fmt.code ''block-out-from = "screen-capture";''}, this ${surface} is blocked out from the screencast portal, and will not be visible to screencasting software making use of the screencast portal.
723
+
''
724
+
''
725
+
The ${fmt.code "wlr-screencopy"} protocol, which is used by tools like ${fmt.code "grim"} primarily to capture screenshots. When ${fmt.code ''block-out-from = "screencast";''}, this protocol is not affected and tools like ${fmt.code "grim"} can still capture the ${surface} just fine. This is because you may still want to take a screenshot of such ${surfaces}. However, some screenshot tools display a fullscreen overlay with a frozen image of the screen, and then capture that. This overlay is ${fmt.em "not"} blocked out in the same way, and may leak the ${surface} contents to an active screencast. When ${fmt.code ''block-out-from = "screen-capture";''}, this ${surface} is blocked out from ${fmt.code "wlr-screencopy"} and thus will never leak in such a case, but of course it will always be blocked out from screenshots and (sometimes) the physical screen.
726
+
''
727
+
''
728
+
The built in ${fmt.code "screenshot"} action, implemented in niri itself. This tool works similarly to those based on ${fmt.code "wlr-screencopy"}, but being a part of the compositor gets superpowers regarding secrecy of ${surface} contents. Its frozen overlay will never leak ${surface} contents to an active screencast, because information of blocked ${surfaces} and can be distinguished for the physical output and screencasts. ${fmt.code "block-out-from"} does not affect the built in screenshot tool at all, and you can always take a screenshot of any ${surface}.
729
+
''
730
+
]}
731
+
732
+
${fmt.table {
733
+
headers = [
734
+
(fmt.code "block-out-from")
735
+
"can ${fmt.code "ScreenCast"}?"
736
+
"can ${fmt.code "screencopy"}?"
737
+
"can ${fmt.code "screenshot"}?"
738
+
];
739
+
align = [
740
+
null
741
+
"center"
742
+
"center"
743
+
"center"
744
+
];
745
+
rows = [
746
+
[
747
+
(fmt.code "null")
748
+
"yes"
749
+
"yes"
750
+
"yes"
751
+
]
752
+
[
753
+
(fmt.code ''"screencast"'')
754
+
"no"
755
+
"yes"
756
+
"yes"
757
+
]
758
+
[
759
+
(fmt.code ''"screen-capture"'')
760
+
"no"
761
+
"no"
762
+
"yes"
763
+
]
764
+
];
765
+
}}
766
+
767
+
${fmt.admonition.caution ''
768
+
${fmt.strong "Streamers: Do not accidentally leak ${surface} contents via screenshots."}
769
+
770
+
For ${surfaces} where ${fmt.code ''block-out-from = "screencast";''}, contents of a ${surface} may still be visible in a screencast, if the ${surface} is indirectly displayed by a tool using ${fmt.code "wlr-screencopy"}.
771
+
772
+
If you are a streamer, either:
773
+
${fmt.list [
774
+
"make sure not to use ${fmt.code "wlr-screencopy"} tools that display a preview during your stream, or"
775
+
(fmt.strong "set ${fmt.code ''block-out-from = "screen-capture";''} to ensure that the ${surface} is never visible in a screencast.")
776
+
]}
777
+
''}
778
+
779
+
${fmt.admonition.caution ''
780
+
${fmt.strong "Do not let malicious ${fmt.code "wlr-screencopy"} clients capture your top secret ${surfaces}."}
781
+
782
+
(and don't let malicious software run on your system in the first place, you silly goose)
783
+
784
+
For ${surfaces} where ${fmt.code ''block-out-from = "screencast";''}, contents of a ${surface} will still be visible to any application using ${fmt.code "wlr-screencopy"}, even if you did not consent to this application capturing your screen.
785
+
786
+
Note that sandboxed clients restricted via security context (i.e. Flatpaks) do not have access to ${fmt.code "wlr-screencopy"} at all, and are not a concern.
787
+
788
+
${fmt.strong "If a ${surface}'s contents are so secret that they must never be captured by any (non-sandboxed) application, set ${fmt.code ''block-out-from = "screen-capture";''}."}
789
+
''}
790
+
791
+
Essentially, use ${fmt.code ''block-out-from = "screen-capture";''} if you want to be sure that the ${surface} is never visible to any external tool no matter what; or use ${fmt.code ''block-out-from = "screencast";''} if you want to be able to capture screenshots of the ${surface} without its contents normally being visible in a screencast. (at the risk of some tools still leaking the ${surface} contents, see above)
792
+
'';
793
+
};
794
+
795
+
alphabetize =
796
+
sections:
797
+
lib.mergeAttrsList (
798
+
lib.imap0 (i: section: {
799
+
${builtins.elemAt lib.strings.lowerChars i} = section;
800
+
}) sections
801
+
);
802
+
803
+
ordered-record = ordered-record' null;
804
+
805
+
ordered-record' =
806
+
description: sections:
807
+
types.submoduleWith {
808
+
inherit description;
809
+
shorthandOnlyDefinesConfig = true;
810
+
modules = make-ordered-options sections;
811
+
};
812
+
813
+
make-ordered-options =
814
+
sections:
815
+
let
816
+
grouped = lib.groupBy (s: if s ? __module then "module" else "options") sections;
817
+
818
+
options' = grouped.options or [ ];
819
+
module' = map (builtins.getAttr "__module") grouped.module or [ ];
820
+
821
+
flat-options = lib.mergeAttrsList options';
822
+
823
+
real-options = lib.filterAttrs (_: opt: !(opt ? niri-flake-document-internal)) flat-options;
824
+
825
+
extra-docs-options = lib.filterAttrs (_: opt: opt ? niri-flake-document-internal) flat-options;
826
+
in
827
+
module'
828
+
++ [
829
+
{
830
+
options = real-options;
831
+
}
832
+
{
833
+
options._module.niri-flake-ordered-record = {
834
+
ordering = lib.mkOption {
835
+
internal = true;
836
+
# readOnly = true;
837
+
visible = false;
838
+
description = ''
839
+
Used to influence the order of options in the documentation, such that they are not always sorted alphabetically.
840
+
841
+
Does not affect any other functionality.
842
+
'';
843
+
default = builtins.concatMap builtins.attrNames options';
844
+
};
845
+
846
+
inherit extra-docs-options;
847
+
};
848
+
}
849
+
850
+
];
851
+
852
+
make-section = type: optional type { };
853
+
854
+
section' = flip pipe [
855
+
submodule
856
+
make-section
857
+
];
858
+
section = flip pipe [
859
+
record
860
+
make-section
861
+
];
862
+
ordered-section = flip pipe [
863
+
ordered-record
864
+
make-section
865
+
];
866
+
in
867
+
submodule (
868
+
{ options, ... }:
869
+
{
870
+
# config._module.niri-flake-ordered-record.ordering = lib.mkForce [
871
+
# "input"
872
+
# "outputs"
873
+
# "binds"
874
+
# "switch-events"
875
+
# "layout"
876
+
877
+
# "workspaces"
878
+
879
+
# "spawn-at-startup"
880
+
# "prefer-no-csd"
881
+
# "screenshot-path"
882
+
# "environment"
883
+
# "overview"
884
+
# "cursor"
885
+
# "xwayland-satellite"
886
+
# "clipboard"
887
+
# "hotkey-overlay"
888
+
889
+
# "window-rules"
890
+
# "layer-rules"
891
+
# "animations"
892
+
# "gestures"
893
+
894
+
# "debug"
895
+
# ];
896
+
imports = make-ordered-options [
897
+
{
898
+
switch-events =
899
+
let
900
+
switch-bind = record' "niri switch bind" {
901
+
action = required (rename "niri switch action" kdl.types.kdl-leaf) // {
902
+
description = ''
903
+
A switch action is represented as an attrset with a single key, being the name, and a value that is a list of its arguments.
904
+
905
+
See also ${link-opt ((subopts options.binds).action)} for more information on how this works, it has the exact same option type. Beware that switch binds are not the same as regular binds, and the actions they take are different. Currently, they can only accept spawn binds. Correct usage is like so:
906
+
907
+
${fmt.nix-code-block ''
908
+
{
909
+
${options.switch-events} = {
910
+
tablet-mode-on.action.spawn = ["gsettings" "set" "org.gnome.desktop.a11y.applications" "screen-keyboard-enabled" "true"];
911
+
tablet-mode-off.action.spawn = ["gsettings" "set" "org.gnome.desktop.a11y.applications" "screen-keyboard-enabled" "false"];
912
+
};
913
+
}
914
+
''}
915
+
'';
916
+
};
917
+
};
918
+
919
+
switch-bind' = nullable (shorthand-for "switch-bind" switch-bind) // {
920
+
visible = "shallow";
921
+
};
922
+
in
923
+
ordered-section [
924
+
{
925
+
tablet-mode-on = switch-bind';
926
+
tablet-mode-off = switch-bind';
927
+
lid-open = switch-bind';
928
+
lid-close = switch-bind';
929
+
}
930
+
{
931
+
"<switch-bind>" = docs-only switch-bind // {
932
+
override-loc = lib.const [ "<switch-bind>" ];
933
+
description = ''
934
+
<!--
935
+
This description doesn't matter to the docs, but is necessary to make this header actually render so the above types can link to it.
936
+
-->
937
+
'';
938
+
};
939
+
}
940
+
];
941
+
binds = attrs-record' "niri keybind" {
942
+
allow-when-locked = optional types.bool false // {
943
+
description = ''
944
+
Whether this keybind should be allowed when the screen is locked.
945
+
946
+
This is only applicable for ${fmt.code "spawn"} keybinds.
947
+
'';
948
+
};
949
+
allow-inhibiting = optional types.bool true // {
950
+
description = ''
951
+
When a surface is inhibiting keyboard shortcuts, this option dictates wether ${fmt.em "this"} keybind will be inhibited as well.
952
+
953
+
By default it is true for all keybinds, meaning an application can block this keybind from being triggered, and the application will receive the key event instead.
954
+
955
+
When false, this keybind will always be triggered, even if an application is inhibiting keybinds. There is no way for a client to observe this keypress.
956
+
957
+
Has no effect when ${fmt.code "action"} is ${fmt.code "toggle-keyboard-shortcuts-inhibit"}. In that case, this value is implicitly false, no matter what you set it to. (note that the value reported in the nix config may be inaccurate in that case; although hopefully you're not relying on the values of specific keybinds for the rest of your config?)
958
+
'';
959
+
};
960
+
cooldown-ms = nullable types.int // {
961
+
description = ''
962
+
The minimum cooldown before a keybind can be triggered again, in milliseconds.
963
+
964
+
This is mostly useful for binds on the mouse wheel, where you might not want to activate an action several times in quick succession. You can use it for any bind, though.
965
+
'';
966
+
};
967
+
repeat = optional types.bool true // {
968
+
description = ''
969
+
Whether this keybind should trigger repeatedly when held down.
970
+
'';
971
+
};
972
+
hotkey-overlay =
973
+
optional
974
+
(types.attrTag {
975
+
hidden = lib.mkOption {
976
+
type = types.bool;
977
+
description = ''
978
+
When ${fmt.code "true"}, the hotkey overlay will not contain this keybind at all. When ${fmt.code "false"}, it will show the default title of the action.
979
+
'';
980
+
};
981
+
title = lib.mkOption {
982
+
type = types.str;
983
+
description = ''
984
+
The title of this keybind in the hotkey overlay. ${
985
+
fmt.masked-link {
986
+
href = "https://docs.gtk.org/Pango/pango_markup.html";
987
+
content = "Pango markup";
988
+
}
989
+
} is supported.
990
+
'';
991
+
};
992
+
})
993
+
{
994
+
hidden = false;
995
+
}
996
+
// {
997
+
description = ''
998
+
How this keybind should be displayed in the hotkey overlay.
999
+
1000
+
${fmt.list [
1001
+
''
1002
+
By default, ${fmt.code "{hidden = false;}"} maps to omitting this from the KDL config; the default title of the action will be used.
1003
+
''
1004
+
''
1005
+
${fmt.code "{hidden = true;}"} will emit ${fmt.code "hotkey-overlay-title=null"} in the KDL config, and the hotkey overlay will not contain this keybind at all.
1006
+
''
1007
+
''
1008
+
${fmt.code ''{title = "foo";}''} will emit ${fmt.code ''hotkey-overlay-title="foo"''} in the KDL config, and the hotkey overlay will show "foo" as the title of this keybind.
1009
+
''
1010
+
]}
1011
+
'';
1012
+
};
1013
+
action = required (rename "niri action" kdl.types.kdl-leaf) // {
1014
+
description = ''
1015
+
An action is represented as an attrset with a single key, being the name, and a value that is a list of its arguments. For example, to represent a spawn action, you could do this:
1016
+
1017
+
${fmt.nix-code-block ''
1018
+
{
1019
+
${options.binds} = {
1020
+
"XF86AudioRaiseVolume".action.spawn = ["wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"];
1021
+
"XF86AudioLowerVolume".action.spawn = ["wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"];
1022
+
};
1023
+
}
1024
+
''}
1025
+
1026
+
If there is only a single argument, you can pass it directly. It will be implicitly converted to a list in that case.
1027
+
1028
+
${fmt.nix-code-block ''
1029
+
{
1030
+
${options.binds} = {
1031
+
"Mod+D".action.spawn = "fuzzel";
1032
+
"Mod+1".action.focus-workspace = 1;
1033
+
};
1034
+
}
1035
+
''}
1036
+
1037
+
For actions taking properties (named arguments), you can pass an attrset.
1038
+
1039
+
${fmt.nix-code-block ''
1040
+
{
1041
+
${options.binds} = {
1042
+
"Mod+Shift+E".action.quit.skip-confirmation = true;
1043
+
"Mod+Print".action.screenshot-screen = { show-pointer = false; };
1044
+
};
1045
+
}
1046
+
''}
1047
+
1048
+
If an action takes properties and positional arguments, you can write it like this:
1049
+
1050
+
${fmt.nix-code-block ''
1051
+
{
1052
+
${options.binds} = {
1053
+
"Mod+Ctrl+1".action.move-window-to-workspace = [ { focus = false; } "chat-apps" ];
1054
+
};
1055
+
}
1056
+
''}
1057
+
'';
1058
+
};
1059
+
};
1060
+
}
1061
+
1062
+
{
1063
+
screenshot-path =
1064
+
optional (nullOr types.str) "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
1065
+
// {
1066
+
description = ''
1067
+
The path to save screenshots to.
1068
+
1069
+
If this is null, then no screenshots will be saved.
1070
+
1071
+
If the path starts with a ${fmt.code "~"}, then it will be expanded to the user's home directory.
1072
+
1073
+
The path is then passed to ${
1074
+
fmt.masked-link {
1075
+
href = "https://man7.org/linux/man-pages/man3/strftime.3.html";
1076
+
content = fmt.code "strftime(3)";
1077
+
}
1078
+
} with the current time, and the result is used as the final path.
1079
+
'';
1080
+
};
1081
+
}
1082
+
1083
+
{
1084
+
hotkey-overlay = {
1085
+
skip-at-startup = optional types.bool false // {
1086
+
description = ''
1087
+
Whether to skip the hotkey overlay shown when niri starts.
1088
+
'';
1089
+
};
1090
+
1091
+
hide-not-bound = optional types.bool false // {
1092
+
description = ''
1093
+
By default, niri has a set of important keybinds that are always shown in the hotkey overlay, even if they are not bound to any key.
1094
+
In particular, this helps new users discover important keybinds, especially if their config has no keybinds at all.
1095
+
1096
+
You can disable this behaviour by setting this option to ${fmt.code "true"}. Then, niri will only show keybinds that are actually bound to a key.
1097
+
'';
1098
+
};
1099
+
};
1100
+
}
1101
+
{
1102
+
config-notification = {
1103
+
disable-failed = optional types.bool false // {
1104
+
description = ''
1105
+
Disable the notification that the config file failed to load.
1106
+
'';
1107
+
};
1108
+
};
1109
+
}
1110
+
1111
+
{
1112
+
clipboard.disable-primary = optional types.bool false // {
1113
+
description = ''
1114
+
The "primary selection" is a special clipboard that contains the text that was last selected with the mouse, and can usually be pasted with the middle mouse button.
1115
+
1116
+
This is a feature that is not inherently part of the core Wayland protocol, but ${
1117
+
fmt.masked-link {
1118
+
href = "https://wayland.app/protocols/primary-selection-unstable-v1#compositor-support";
1119
+
content = "a widely supported protocol extension";
1120
+
}
1121
+
} enables support for it anyway.
1122
+
1123
+
This functionality was inherited from X11, is not necessarily intuitive to many users; especially those coming from other operating systems that do not have this feature (such as Windows, where the middle mouse button is used for scrolling).
1124
+
1125
+
If you don't want to have a primary selection, you can disable it with this option. Doing so will prevent niri from adveritising support for the primary selection protocol.
1126
+
1127
+
Note that this option has nothing to do with the "clipboard" that is commonly invoked with ${fmt.kbd "Ctrl+C"} and ${fmt.kbd "Ctrl+V"}.
1128
+
'';
1129
+
};
1130
+
}
1131
+
1132
+
{
1133
+
prefer-no-csd = optional types.bool false // {
1134
+
description = ''
1135
+
Whether to prefer server-side decorations (SSD) over client-side decorations (CSD).
1136
+
'';
1137
+
};
1138
+
}
1139
+
1140
+
{
1141
+
spawn-at-startup =
1142
+
list (
1143
+
types.attrTag {
1144
+
argv = lib.mkOption {
1145
+
type = types.listOf types.str;
1146
+
description = ''
1147
+
Almost raw process arguments to spawn, without shell syntax.
1148
+
1149
+
A leading tilde in the zeroth argument will be expanded to the user's home directory. No other preprocessing is applied.
1150
+
1151
+
Usage is like so:
1152
+
1153
+
${fmt.nix-code-block ''
1154
+
{
1155
+
${options.spawn-at-startup} = [
1156
+
{ argv = ["waybar"]; }
1157
+
{ argv = ["swaybg" "--image" "/path/to/wallpaper.jpg"]; }
1158
+
{ argv = ["~/.config/niri/scripts/startup.sh"]; }
1159
+
];
1160
+
}
1161
+
''}
1162
+
'';
1163
+
};
1164
+
sh = lib.mkOption {
1165
+
type = types.str;
1166
+
description = ''
1167
+
A shell command to spawn. Run wild with POSIX syntax.
1168
+
1169
+
${fmt.nix-code-block ''
1170
+
{
1171
+
${options.spawn-at-startup} = [
1172
+
{ sh = "echo $NIRI_SOCKET > ~/.niri-socket"; }
1173
+
];
1174
+
}
1175
+
''}
1176
+
1177
+
Note that ${fmt.code ''{ sh = "foo"; }''} is exactly equivalent to ${fmt.code ''{ argv = [ "sh" "-c" "foo" ]; }''}.
1178
+
'';
1179
+
};
1180
+
1181
+
# alias of argv
1182
+
command = lib.mkOption {
1183
+
type = types.listOf types.str;
1184
+
visible = false;
1185
+
};
1186
+
}
1187
+
)
1188
+
// {
1189
+
description = ''
1190
+
A list of commands to run when niri starts.
1191
+
1192
+
Each command can be represented as its raw arguments, or as a shell invocation.
1193
+
1194
+
When niri is built with the ${fmt.code "systemd"} feature (on by default), commands spawned this way (or with the ${fmt.code "spawn"} and ${fmt.code "spawn-sh"} actions) will be put in a transient systemd unit, which separates the process from niri and prevents e.g. OOM situations from killing the entire session.
1195
+
'';
1196
+
};
1197
+
}
1198
+
1199
+
{
1200
+
workspaces =
1201
+
attrs-record (key: {
1202
+
name = optional types.str key // {
1203
+
defaultText = "the key of the workspace";
1204
+
description = ''
1205
+
The name of the workspace. You set this manually if you want the keys to be ordered in a specific way.
1206
+
'';
1207
+
};
1208
+
open-on-output = nullable types.str // {
1209
+
description = ''
1210
+
The name of the output the workspace should be assigned to.
1211
+
'';
1212
+
};
1213
+
})
1214
+
// {
1215
+
description = ''
1216
+
Declare named workspaces.
1217
+
1218
+
Named workspaces are similar to regular, dynamic workspaces, except they can be
1219
+
referred to by name, and they are persistent, they do not close when there are
1220
+
no more windows left on them.
1221
+
1222
+
Usage is like so:
1223
+
1224
+
${fmt.nix-code-block ''
1225
+
{
1226
+
${options.workspaces}."name" = {};
1227
+
${options.workspaces}."01-another-one" = {
1228
+
open-on-output = "DP-1";
1229
+
name = "another-one";
1230
+
};
1231
+
}
1232
+
''}
1233
+
1234
+
Unless a ${fmt.code "name"} is declared, the workspace will use the attribute key as the name.
1235
+
1236
+
Workspaces will be created in a specific order: sorted by key. If you do not care
1237
+
about the order of named workspaces, you can skip using the ${fmt.code "name"} attribute, and
1238
+
use the key instead. If you do care about it, you can use the key to order them,
1239
+
and a ${fmt.code "name"} attribute to have a friendlier name.
1240
+
'';
1241
+
};
1242
+
}
1243
+
1244
+
{
1245
+
overview = {
1246
+
zoom = nullable float-or-int // {
1247
+
description = ''
1248
+
Control how much the workspaces zoom out in the overview. zoom ranges from 0 to 0.75 where lower values make everything smaller.
1249
+
'';
1250
+
};
1251
+
backdrop-color = nullable types.str // {
1252
+
description = ''
1253
+
Set the backdrop color behind workspaces in the overview. The backdrop is also visible between workspaces when switching.
1254
+
1255
+
The alpha channel for this color will be ignored.
1256
+
'';
1257
+
};
1258
+
1259
+
workspace-shadow = {
1260
+
enable = optional types.bool true;
1261
+
offset =
1262
+
nullable (record {
1263
+
x = optional float-or-int 0.0;
1264
+
y = optional float-or-int 5.0;
1265
+
})
1266
+
// {
1267
+
description = shadow-descriptions.offset;
1268
+
};
1269
+
1270
+
softness = nullable float-or-int // {
1271
+
description = shadow-descriptions.softness;
1272
+
};
1273
+
1274
+
spread = nullable float-or-int // {
1275
+
description = shadow-descriptions.spread;
1276
+
};
1277
+
1278
+
color = nullable types.str;
1279
+
};
1280
+
};
1281
+
}
1282
+
1283
+
{
1284
+
input = {
1285
+
keyboard = {
1286
+
xkb =
1287
+
let
1288
+
arch-man-xkb =
1289
+
anchor:
1290
+
fmt.masked-link {
1291
+
href = "https://man.archlinux.org/man/xkeyboard-config.7#${anchor}";
1292
+
content = fmt.code "xkeyboard-config(7)";
1293
+
};
1294
+
1295
+
default-env = default: field: ''
1296
+
If this is set to ${default}, the ${field} will be read from the ${fmt.code "XKB_DEFAULT_${lib.toUpper field}"} environment variable.
1297
+
'';
1298
+
1299
+
str-fallback = default-env "an empty string";
1300
+
nullable-fallback = default-env "null";
1301
+
1302
+
base = {
1303
+
layout = optional types.str "" // {
1304
+
description = ''
1305
+
A comma-separated list of layouts (languages) to include in the keymap.
1306
+
1307
+
See ${arch-man-xkb "LAYOUTS"} for a list of available layouts and their variants.
1308
+
1309
+
${str-fallback "layout"}
1310
+
'';
1311
+
};
1312
+
model = optional types.str "" // {
1313
+
description = ''
1314
+
The keyboard model by which to interpret keycodes and LEDs
1315
+
1316
+
See ${arch-man-xkb "MODELS"} for a list of available models.
1317
+
1318
+
${str-fallback "model"}
1319
+
'';
1320
+
};
1321
+
rules = optional types.str "" // {
1322
+
description = ''
1323
+
The rules file to use.
1324
+
1325
+
The rules file describes how to interpret the values of the model, layout, variant and options fields.
1326
+
1327
+
${str-fallback "rules"}
1328
+
'';
1329
+
};
1330
+
variant = optional types.str "" // {
1331
+
description = ''
1332
+
A comma separated list of variants, one per layout, which may modify or augment the respective layout in various ways.
1333
+
1334
+
See ${arch-man-xkb "LAYOUTS"} for a list of available variants for each layout.
1335
+
1336
+
${str-fallback "variant"}
1337
+
'';
1338
+
};
1339
+
options = nullable types.str // {
1340
+
description = ''
1341
+
A comma separated list of options, through which the user specifies non-layout related preferences, like which key combinations are used for switching layouts, or which key is the Compose key.
1342
+
1343
+
See ${arch-man-xkb "OPTIONS"} for a list of available options.
1344
+
1345
+
If this is set to an empty string, no options will be used.
1346
+
1347
+
${nullable-fallback "options"}
1348
+
'';
1349
+
};
1350
+
};
1351
+
# base' = mapAttrs (name: opt: opt // optionalAttrs (opt.default == "" || opt.default == null) {defaultText = "${if opt.default == "" then "\"\"" else "null"} (inherited from XKB_DEFAULT_${toUpper name}>";}) base;
1352
+
in
1353
+
ordered-section [
1354
+
{
1355
+
file = nullable types.str // {
1356
+
description = ''
1357
+
Path to a ${fmt.code ".xkb"} keymap file. If set, this file will be used to configure libxkbcommon, and all other options will be ignored.
1358
+
'';
1359
+
};
1360
+
}
1361
+
base
1362
+
]
1363
+
// {
1364
+
description = ''
1365
+
Parameters passed to libxkbcommon, which handles the keyboard in niri.
1366
+
1367
+
Further reading:
1368
+
${fmt.list [
1369
+
(fmt.masked-link {
1370
+
href = "https://docs.rs/smithay/latest/smithay/wayland/seat/struct.XkbConfig.html";
1371
+
content = fmt.code "smithay::wayland::seat::XkbConfig";
1372
+
})
1373
+
]}
1374
+
'';
1375
+
};
1376
+
repeat-delay = optional types.int 600 // {
1377
+
description = ''
1378
+
The delay in milliseconds before a key starts repeating.
1379
+
'';
1380
+
};
1381
+
repeat-rate = optional types.int 25 // {
1382
+
description = ''
1383
+
The rate in characters per second at which a key repeats.
1384
+
'';
1385
+
};
1386
+
track-layout =
1387
+
optional (enum [
1388
+
"global"
1389
+
"window"
1390
+
]) "global"
1391
+
// {
1392
+
description = ''
1393
+
The keyboard layout can be remembered per ${fmt.code ''"window"''}, such that when you switch to a window, the keyboard layout is set to the one that was last used in that window.
1394
+
1395
+
By default, there is only one ${fmt.code ''"global"''} keyboard layout and changing it in any window will affect the keyboard layout used in all other windows too.
1396
+
'';
1397
+
};
1398
+
numlock = optional types.bool false // {
1399
+
description = ''
1400
+
Enable numlock by default
1401
+
'';
1402
+
};
1403
+
};
1404
+
touchpad =
1405
+
pointer-tablet-common
1406
+
// basic-pointer true
1407
+
// {
1408
+
tap = optional types.bool true // {
1409
+
description = ''
1410
+
Whether to enable tap-to-click.
1411
+
1412
+
Further reading:
1413
+
${fmt.list [
1414
+
(libinput-link "configuration" "Tap-to-click")
1415
+
(libinput-link "tapping" "Tap-to-click behaviour")
1416
+
]}
1417
+
'';
1418
+
};
1419
+
dwt = optional types.bool false // {
1420
+
description = ''
1421
+
Whether to disable the touchpad while typing.
1422
+
1423
+
Further reading:
1424
+
${fmt.list [
1425
+
(libinput-link "configuration" "Disable while typing")
1426
+
(libinput-link "palm-detection" "Disable-while-typing")
1427
+
]}
1428
+
'';
1429
+
};
1430
+
dwtp = optional types.bool false // {
1431
+
description = ''
1432
+
Whether to disable the touchpad while the trackpoint is in use.
1433
+
1434
+
Further reading:
1435
+
${fmt.list [
1436
+
(libinput-link "configuration" "Disable while trackpointing")
1437
+
(libinput-link "palm-detection" "Disable-while-trackpointing")
1438
+
]}
1439
+
'';
1440
+
};
1441
+
drag = nullable types.bool // {
1442
+
description = ''
1443
+
On most touchpads, "tap and drag" is enabled by default. This option allows you to explicitly enable or disable it.
1444
+
1445
+
Tap and drag means that to drag an item, you tap the touchpad with some amount of fingers to decide what kind of button press is emulated, but don't hold those fingers, and then you immediately start dragging with one finger.
1446
+
1447
+
Further reading:
1448
+
${fmt.list [
1449
+
(libinput-link "tapping" "Tap-and-drag")
1450
+
]}
1451
+
'';
1452
+
};
1453
+
drag-lock = optional types.bool false // {
1454
+
description = ''
1455
+
By default, a "tap and drag" gesture is terminated by releasing the finger that is dragging.
1456
+
1457
+
Drag lock means that the drag gesture is not terminated when the finger is released, but only when the finger is tapped again, or after a timeout (unless sticky mode is enabled). This allows you to reset your finger position without losing the drag gesture.
1458
+
1459
+
Drag lock is only applicable when tap and drag is enabled.
1460
+
1461
+
Further reading:
1462
+
${fmt.list [
1463
+
(libinput-link "tapping" "Tap-and-drag")
1464
+
]}
1465
+
'';
1466
+
};
1467
+
1468
+
disabled-on-external-mouse = optional types.bool false // {
1469
+
description = ''
1470
+
Whether to disable the touchpad when an external mouse is plugged in.
1471
+
1472
+
Further reading:
1473
+
${fmt.list [
1474
+
(libinput-link "configuration" "Send Events Mode")
1475
+
]}
1476
+
'';
1477
+
};
1478
+
tap-button-map =
1479
+
nullable (enum [
1480
+
"left-middle-right"
1481
+
"left-right-middle"
1482
+
])
1483
+
// {
1484
+
description = ''
1485
+
The mouse button to register when tapping with 1, 2, or 3 fingers, when ${link-opt options.input.touchpad.tap} is enabled.
1486
+
1487
+
Further reading:
1488
+
${fmt.list [
1489
+
(libinput-link "configuration" "Tap-to-click")
1490
+
]}
1491
+
'';
1492
+
};
1493
+
click-method =
1494
+
nullable (enum [
1495
+
"button-areas"
1496
+
"clickfinger"
1497
+
])
1498
+
// {
1499
+
description = ''
1500
+
Method to determine which mouse button is pressed when you click the touchpad.
1501
+
1502
+
${fmt.list [
1503
+
''
1504
+
${fmt.code ''"button-areas"''}: ${libinput-doc "clickpad-softbuttons" "Software button areas"} \
1505
+
The button is determined by which part of the touchpad was clicked.
1506
+
''
1507
+
''
1508
+
${fmt.code ''"clickfinger"''}: ${libinput-doc "clickpad-softbuttons" "Clickfinger behavior"} \
1509
+
The button is determined by how many fingers clicked.
1510
+
''
1511
+
]}
1512
+
1513
+
Further reading:
1514
+
${fmt.list [
1515
+
(libinput-link "configuration" "Click method")
1516
+
(libinput-link "clickpad-softbuttons" "Clickpad software button behavior")
1517
+
]}
1518
+
'';
1519
+
};
1520
+
1521
+
scroll-factor =
1522
+
nullable (
1523
+
types.either float-or-int (record {
1524
+
horizontal = optional float-or-int 1.0;
1525
+
vertical = optional float-or-int 1.0;
1526
+
})
1527
+
)
1528
+
// {
1529
+
description = ''
1530
+
For all scroll events triggered by a finger source, the scroll distance is multiplied by this factor.
1531
+
1532
+
This is not a libinput property, but rather a niri-specific one.
1533
+
'';
1534
+
};
1535
+
};
1536
+
mouse =
1537
+
pointer-tablet-common
1538
+
// basic-pointer false
1539
+
// {
1540
+
scroll-factor =
1541
+
nullable (
1542
+
types.either float-or-int (record {
1543
+
horizontal = optional float-or-int 1.0;
1544
+
vertical = optional float-or-int 1.0;
1545
+
})
1546
+
)
1547
+
// {
1548
+
description = ''
1549
+
For all scroll events triggered by a wheel source, the scroll distance is multiplied by this factor.
1550
+
1551
+
This is not a libinput property, but rather a niri-specific one.
1552
+
'';
1553
+
};
1554
+
};
1555
+
trackpoint = pointer-tablet-common // basic-pointer false;
1556
+
trackball = pointer-tablet-common // basic-pointer false;
1557
+
tablet = pointer-tablet-common // {
1558
+
map-to-output = nullable types.str;
1559
+
calibration-matrix =
1560
+
nullable (mkOptionType {
1561
+
name = "matrix";
1562
+
description = "2x3 matrix";
1563
+
check =
1564
+
matrix:
1565
+
builtins.isList matrix
1566
+
&& builtins.length matrix == 2
1567
+
&& builtins.all (
1568
+
row: builtins.isList row && builtins.length row == 3 && builtins.all builtins.isFloat row
1569
+
) matrix;
1570
+
merge = lib.mergeUniqueOption {
1571
+
message = "";
1572
+
merge = loc: defs: builtins.concatLists (builtins.head defs).value;
1573
+
};
1574
+
})
1575
+
// {
1576
+
description = ''
1577
+
An augmented calibration matrix for the tablet.
1578
+
1579
+
This is represented in Nix as a 2-list of 3-lists of floats.
1580
+
1581
+
For example:
1582
+
${fmt.nix-code-block ''
1583
+
{
1584
+
# 90 degree rotation clockwise
1585
+
calibration-matrix = [
1586
+
[ 0.0 -1.0 1.0 ]
1587
+
[ 1.0 0.0 0.0 ]
1588
+
];
1589
+
}
1590
+
''}
1591
+
1592
+
Further reading:
1593
+
${fmt.list [
1594
+
(fmt.masked-link {
1595
+
href = "https://wayland.freedesktop.org/libinput/doc/1.8.2/group__config.html#ga3d9f1b9be10e804e170c4ea455bd1f1b";
1596
+
content = fmt.code "libinput_device_config_calibration_get_default_matrix()";
1597
+
})
1598
+
(fmt.masked-link {
1599
+
href = "https://wayland.freedesktop.org/libinput/doc/1.8.2/group__config.html#ga09a798f58cc601edd2797780096e9804";
1600
+
content = fmt.code "libinput_device_config_calibration_set_matrix()";
1601
+
})
1602
+
(fmt.masked-link {
1603
+
href = "https://smithay.github.io/smithay/input/struct.Device.html#method.config_calibration_set_matrix";
1604
+
content = "rustdoc because libinput's web docs are an eyesore";
1605
+
})
1606
+
]}
1607
+
'';
1608
+
};
1609
+
};
1610
+
touch.enable = optional types.bool true;
1611
+
touch.map-to-output = nullable types.str;
1612
+
warp-mouse-to-focus =
1613
+
let
1614
+
inner = record {
1615
+
enable = optional types.bool false;
1616
+
mode = nullable types.str;
1617
+
};
1618
+
1619
+
actual-type = mkOptionType {
1620
+
inherit (inner)
1621
+
name
1622
+
description
1623
+
getSubOptions
1624
+
nestedTypes
1625
+
;
1626
+
1627
+
check = value: builtins.isBool value || inner.check value;
1628
+
merge =
1629
+
loc: defs:
1630
+
lib.warnIf (builtins.any (def: builtins.isBool def.value) defs)
1631
+
(rename-warning loc (loc ++ [ "enable" ]) (builtins.filter (def: builtins.isBool def.value) defs))
1632
+
inner.merge
1633
+
loc
1634
+
(map (def: if builtins.isBool def.value then def // { value.enable = def.value; } else def) defs);
1635
+
};
1636
+
in
1637
+
optional actual-type { }
1638
+
// {
1639
+
description = ''
1640
+
Whether to warp the mouse to the focused window when switching focus.
1641
+
'';
1642
+
};
1643
+
focus-follows-mouse.enable = optional types.bool false // {
1644
+
description = ''
1645
+
Whether to focus the window under the mouse when the mouse moves.
1646
+
'';
1647
+
};
1648
+
focus-follows-mouse.max-scroll-amount = nullable types.str // {
1649
+
description = ''
1650
+
The maximum proportion of the screen to scroll at a time
1651
+
'';
1652
+
};
1653
+
1654
+
workspace-auto-back-and-forth = optional types.bool false // {
1655
+
description = ''
1656
+
When invoking ${fmt.code "focus-workspace"} to switch to a workspace by index, if the workspace is already focused, usually nothing happens. When this option is enabled, the workspace will cycle back to the previously active workspace.
1657
+
1658
+
Of note is that it does not switch to the previous ${fmt.em "index"}, but the previous ${fmt.em "workspace"}. That means you can reorder workspaces inbetween these actions, and it will still take you to the actual same workspace you came from.
1659
+
'';
1660
+
};
1661
+
1662
+
power-key-handling.enable = optional types.bool true // {
1663
+
description = ''
1664
+
By default, niri will take over the power button to make it sleep instead of power off.
1665
+
1666
+
You can disable this behaviour if you prefer to configure the power button elsewhere.
1667
+
'';
1668
+
};
1669
+
1670
+
mod-key = nullable types.str;
1671
+
mod-key-nested = nullable types.str;
1672
+
};
1673
+
}
1674
+
1675
+
{
1676
+
outputs = attrs-record (key: {
1677
+
name = optional types.str key // {
1678
+
defaultText = "the key of the output";
1679
+
description = ''
1680
+
The name of the output. You set this manually if you want the outputs to be ordered in a specific way.
1681
+
'';
1682
+
};
1683
+
enable = optional types.bool true;
1684
+
backdrop-color = nullable types.str // {
1685
+
description = ''
1686
+
The backdrop color that niri draws for this output. This is visible between workspaces or in the overview.
1687
+
'';
1688
+
};
1689
+
background-color = nullable types.str // {
1690
+
description = ''
1691
+
The background color of this output. This is equivalent to launching ${fmt.code "swaybg -c <color>"} on that output, but is handled by the compositor itself for solid colors.
1692
+
'';
1693
+
};
1694
+
scale = nullable float-or-int // {
1695
+
description = ''
1696
+
The scale of this output, which represents how many physical pixels fit in one logical pixel.
1697
+
1698
+
If this is null, niri will automatically pick a scale for you.
1699
+
'';
1700
+
};
1701
+
transform = {
1702
+
flipped = optional types.bool false // {
1703
+
description = ''
1704
+
Whether to flip this output vertically.
1705
+
'';
1706
+
};
1707
+
rotation =
1708
+
optional (enum [
1709
+
0
1710
+
90
1711
+
180
1712
+
270
1713
+
]) 0
1714
+
// {
1715
+
description = ''
1716
+
Counter-clockwise rotation of this output in degrees.
1717
+
'';
1718
+
};
1719
+
};
1720
+
position =
1721
+
nullable (record {
1722
+
x = required types.int;
1723
+
y = required types.int;
1724
+
})
1725
+
// {
1726
+
description = ''
1727
+
Position of the output in the global coordinate space.
1728
+
1729
+
This affects directional monitor actions like "focus-monitor-left", and cursor movement.
1730
+
1731
+
The cursor can only move between directly adjacent outputs.
1732
+
1733
+
Output scale has to be taken into account for positioning, because outputs are sized in logical pixels.
1734
+
1735
+
For example, a 3840x2160 output with scale 2.0 will have a logical size of 1920x1080, so to put another output directly adjacent to it on the right, set its x to 1920.
1736
+
1737
+
If the position is unset or multiple outputs overlap, niri will instead place the output automatically.
1738
+
'';
1739
+
};
1740
+
mode =
1741
+
nullable (record {
1742
+
width = required types.int;
1743
+
height = required types.int;
1744
+
refresh = nullable types.float // {
1745
+
description = ''
1746
+
The refresh rate of this output. When this is null, but the resolution is set, niri will automatically pick the highest available refresh rate.
1747
+
'';
1748
+
};
1749
+
})
1750
+
// {
1751
+
description = ''
1752
+
The resolution and refresh rate of this display.
1753
+
1754
+
By default, when this is null, niri will automatically pick a mode for you.
1755
+
1756
+
If this is set to an invalid mode (i.e unsupported by this output), niri will act as if it is unset and pick one for you.
1757
+
'';
1758
+
};
1759
+
1760
+
variable-refresh-rate =
1761
+
optional (enum [
1762
+
false
1763
+
"on-demand"
1764
+
true
1765
+
]) false
1766
+
// {
1767
+
description = ''
1768
+
Whether to enable variable refresh rate (VRR) on this output.
1769
+
1770
+
VRR is also known as Adaptive Sync, FreeSync, and G-Sync.
1771
+
1772
+
Setting this to ${fmt.code ''"on-demand"''} will enable VRR only when a window with ${link-opt (subopts options.window-rules).variable-refresh-rate} is present on this output.
1773
+
'';
1774
+
};
1775
+
1776
+
focus-at-startup = optional types.bool false // {
1777
+
description = ''
1778
+
Focus this output by default when niri starts.
1779
+
1780
+
If multiple outputs with ${fmt.code "focus-at-startup"} are connected, then the one with the key that sorts first will be focused. You can change the key to affect the sorting order, and set ${link-opt (subopts options.outputs).name} to be the actual name of the output.
1781
+
1782
+
When none of the connected outputs are explicitly focus-at-startup, niri will focus the first one sorted by name (same output sorting as used elsewhere in niri).
1783
+
'';
1784
+
};
1785
+
});
1786
+
}
1787
+
1788
+
{
1789
+
cursor = section' {
1790
+
imports = [
1791
+
(lib.mkRenamedOptionModule [ "hide-on-key-press" ] [ "hide-when-typing" ])
1792
+
];
1793
+
options = {
1794
+
theme = optional types.str "default" // {
1795
+
description = ''
1796
+
The name of the xcursor theme to use.
1797
+
1798
+
This will also set the XCURSOR_THEME environment variable for all spawned processes.
1799
+
'';
1800
+
};
1801
+
size = optional types.int 24 // {
1802
+
description = ''
1803
+
The size of the cursor in logical pixels.
1804
+
1805
+
This will also set the XCURSOR_SIZE environment variable for all spawned processes.
1806
+
'';
1807
+
};
1808
+
hide-when-typing = optional types.bool false // {
1809
+
description = ''
1810
+
Whether to hide the cursor when typing.
1811
+
'';
1812
+
};
1813
+
hide-after-inactive-ms = nullable types.int // {
1814
+
description = ''
1815
+
If set, the cursor will automatically hide once this number of milliseconds passes since the last cursor movement.
1816
+
'';
1817
+
};
1818
+
};
1819
+
};
1820
+
}
1821
+
1822
+
{
1823
+
layout = ordered-section [
1824
+
{
1825
+
focus-ring = borderish {
1826
+
enable-by-default = true;
1827
+
name = "focus ring";
1828
+
window = "focused window";
1829
+
description = ''
1830
+
The focus ring is a decoration drawn ${fmt.em "around"} the last focused window on each monitor. It takes no space away from windows. If you have insufficient gaps, the focus ring can be drawn over adjacent windows, but it will never affect the layout of windows.
1831
+
1832
+
The focused window of the currently focused monitor, i.e. the window that can receive keyboard input, will be drawn according to ${link-opt (subopts (subopts options.layout).focus-ring).active}, and the last focused window on all other monitors will be drawn according to ${link-opt (subopts (subopts options.layout).focus-ring).inactive}.
1833
+
1834
+
If you have ${link-opt (subopts options.layout).border} enabled, the focus ring will be drawn around (and under) the border.
1835
+
'';
1836
+
};
1837
+
1838
+
border = borderish {
1839
+
enable-by-default = false;
1840
+
name = "border";
1841
+
window = "window";
1842
+
description = ''
1843
+
The border is a decoration drawn ${fmt.em "inside"} every window in the layout. It will take space away from windows. That is, if you have a border of 8px, then each window will be 8px smaller on each edge than if you had no border.
1844
+
1845
+
The currently focused window, i.e. the window that can receive keyboard input, will be drawn according to ${link-opt (subopts (subopts options.layout).border).active}, and all other windows will be drawn according to ${link-opt (subopts (subopts options.layout).border).inactive}.
1846
+
1847
+
If you have ${link-opt (subopts options.layout).focus-ring} enabled, the border will be drawn inside (and over) the focus ring.
1848
+
'';
1849
+
};
1850
+
}
1851
+
{
1852
+
shadow = section {
1853
+
enable = optional types.bool false;
1854
+
offset =
1855
+
section {
1856
+
x = optional float-or-int 0.0;
1857
+
y = optional float-or-int 5.0;
1858
+
}
1859
+
// {
1860
+
description = shadow-descriptions.offset;
1861
+
};
1862
+
1863
+
softness = optional float-or-int 30.0 // {
1864
+
description = shadow-descriptions.softness;
1865
+
};
1866
+
1867
+
spread = optional float-or-int 5.0 // {
1868
+
description = shadow-descriptions.spread;
1869
+
};
1870
+
1871
+
draw-behind-window = optional types.bool false;
1872
+
1873
+
# 0x70 is 43.75% so let's use hex notation lol
1874
+
color = optional types.str "#00000070";
1875
+
1876
+
inactive-color = nullable types.str;
1877
+
};
1878
+
}
1879
+
{
1880
+
insert-hint =
1881
+
section' (
1882
+
{ options, ... }:
1883
+
{
1884
+
imports = make-ordered-options [
1885
+
{
1886
+
enable = optional types.bool true // {
1887
+
description = ''
1888
+
Whether to enable the insert hint.
1889
+
'';
1890
+
};
1891
+
}
1892
+
(make-decoration-options options {
1893
+
display.description = ''
1894
+
The color of the insert hint.
1895
+
'';
1896
+
})
1897
+
];
1898
+
}
1899
+
)
1900
+
// {
1901
+
description = ''
1902
+
The insert hint is a decoration drawn ${fmt.em "between"} windows during an interactive move operation. It is drawn in the gap where the window will be inserted when you release the window. It does not occupy any space in the gap, and the insert hint extends onto the edges of adjacent windows. When you release the moved window, the windows that are covered by the insert hint will be pushed aside to make room for the moved window.
1903
+
'';
1904
+
};
1905
+
}
1906
+
{
1907
+
"<decoration>" =
1908
+
let
1909
+
self = docs-only (decoration (self // { loc = [ "<decoration>" ]; })) // {
1910
+
override-loc = lib.const [ "<decoration>" ];
1911
+
description = ''
1912
+
A decoration is drawn around a surface, adding additional elements that are not necessarily part of an application, but are part of what we think of as a "window".
1913
+
1914
+
This type specifically represents decorations drawn by niri: that is, ${link-opt (subopts options.layout).focus-ring} and/or ${link-opt (subopts options.layout).border}.
1915
+
'';
1916
+
};
1917
+
in
1918
+
self;
1919
+
}
1920
+
{
1921
+
background-color = nullable types.str // {
1922
+
description = ''
1923
+
The default background color that niri draws for workspaces. This is visible when you're not using any background tools like swaybg.
1924
+
'';
1925
+
};
1926
+
}
1927
+
{
1928
+
preset-column-widths = list preset-width // {
1929
+
description = ''
1930
+
The widths that ${fmt.code "switch-preset-column-width"} will cycle through.
1931
+
1932
+
Each width can either be a fixed width in logical pixels, or a proportion of the screen's width.
1933
+
1934
+
Example:
1935
+
1936
+
${fmt.nix-code-block ''
1937
+
{
1938
+
${(subopts options.layout).preset-column-widths} = [
1939
+
{ proportion = 1. / 3.; }
1940
+
{ proportion = 1. / 2.; }
1941
+
{ proportion = 2. / 3.; }
1942
+
1943
+
# { fixed = 1920; }
1944
+
];
1945
+
}
1946
+
''}
1947
+
'';
1948
+
};
1949
+
preset-window-heights = list preset-height // {
1950
+
description = ''
1951
+
The heights that ${fmt.code "switch-preset-window-height"} will cycle through.
1952
+
1953
+
Each height can either be a fixed height in logical pixels, or a proportion of the screen's height.
1954
+
1955
+
Example:
1956
+
1957
+
${fmt.nix-code-block ''
1958
+
{
1959
+
${(subopts options.layout).preset-window-heights} = [
1960
+
{ proportion = 1. / 3.; }
1961
+
{ proportion = 1. / 2.; }
1962
+
{ proportion = 2. / 3.; }
1963
+
1964
+
# { fixed = 1080; }
1965
+
];
1966
+
}
1967
+
''}
1968
+
'';
1969
+
};
1970
+
}
1971
+
{
1972
+
default-column-width = optional default-width { } // {
1973
+
description = ''
1974
+
The default width for new columns.
1975
+
1976
+
When this is set to an empty attrset ${fmt.code "{}"}, windows will get to decide their initial width. This is not null, such that it can be distinguished from window rules that don't touch this
1977
+
1978
+
See ${link-opt (subopts options.layout).preset-column-widths} for more information.
1979
+
1980
+
You can override this for specific windows using ${link-opt (subopts options.window-rules).default-column-width}
1981
+
'';
1982
+
};
1983
+
center-focused-column =
1984
+
optional (enum [
1985
+
"never"
1986
+
"always"
1987
+
"on-overflow"
1988
+
]) "never"
1989
+
// {
1990
+
description = ''
1991
+
When changing focus, niri can automatically center the focused column.
1992
+
1993
+
${fmt.list [
1994
+
"${fmt.code ''"never"''}: If the focused column doesn't fit, it will be aligned to the edges of the screen."
1995
+
"${fmt.code ''"on-overflow"''}: if the focused column doesn't fit, it will be centered on the screen."
1996
+
"${fmt.code ''"always"''}: the focused column will always be centered, even if it was already fully visible."
1997
+
]}
1998
+
'';
1999
+
};
2000
+
always-center-single-column = optional types.bool false // {
2001
+
description = ''
2002
+
This is like ${fmt.code ''center-focused-column = "always";''}, but only for workspaces with a single column. Changes nothing if ${fmt.code "center-focused-column"} is set to ${fmt.code ''"always"''}. Has no effect if more than one column is present.
2003
+
'';
2004
+
};
2005
+
default-column-display =
2006
+
optional (enum [
2007
+
"normal"
2008
+
"tabbed"
2009
+
]) "normal"
2010
+
// {
2011
+
description = ''
2012
+
How windows in columns should be displayed by default.
2013
+
2014
+
${fmt.list [
2015
+
"${fmt.code ''"normal"''}: Windows are arranged vertically, spread across the working area height."
2016
+
"${fmt.code ''"tabbed"''}: Windows are arranged in tabs, with only the focused window visible, taking up the full height of the working area."
2017
+
]}
2018
+
2019
+
Note that you can override this for a given column at any time. Every column remembers its own display mode, independent from this setting. This setting controls the default value when a column is ${fmt.em "created"}.
2020
+
2021
+
Also, since a newly created column always contains a single window, you can override this default value with ${link-opt (subopts options.window-rules).default-column-display}.
2022
+
'';
2023
+
};
2024
+
2025
+
tab-indicator = nullable (
2026
+
submodule (
2027
+
{ options, ... }:
2028
+
{
2029
+
imports = make-ordered-options [
2030
+
{
2031
+
enable = optional types.bool true;
2032
+
hide-when-single-tab = optional types.bool false;
2033
+
place-within-column = optional types.bool false;
2034
+
gap = optional float-or-int 5.0;
2035
+
width = optional float-or-int 4.0;
2036
+
length.total-proportion = optional types.float 0.5;
2037
+
position = optional (enum [
2038
+
"left"
2039
+
"right"
2040
+
"top"
2041
+
"bottom"
2042
+
]) "left";
2043
+
gaps-between-tabs = optional float-or-int 0.0;
2044
+
corner-radius = optional float-or-int 0.0;
2045
+
}
2046
+
2047
+
(make-decoration-options options {
2048
+
urgent.description = ''
2049
+
The color of the tab indicator for windows that are requesting attention.
2050
+
'';
2051
+
active.description = ''
2052
+
The color of the tab indicator for the window that has keyboard focus.
2053
+
'';
2054
+
inactive.description = ''
2055
+
The color of the tab indicator for windows that do not have keyboard focus.
2056
+
'';
2057
+
})
2058
+
2059
+
];
2060
+
}
2061
+
)
2062
+
);
2063
+
}
2064
+
{
2065
+
empty-workspace-above-first = optional types.bool false // {
2066
+
description = ''
2067
+
Normally, niri has a dynamic amount of workspaces, with one empty workspace at the end. The first workspace really is the first workspace, and you cannot go past it, but going past the last workspace puts you on the empty workspace.
2068
+
2069
+
When this is enabled, there will be an empty workspace above the first workspace, and you can go past the first workspace to get to an empty workspace, just as in the other direction. This makes workspace navigation symmetric in all ways except indexing.
2070
+
'';
2071
+
};
2072
+
gaps = optional float-or-int 16 // {
2073
+
description = ''
2074
+
The gap between windows in the layout, measured in logical pixels.
2075
+
'';
2076
+
};
2077
+
struts =
2078
+
section {
2079
+
left = optional float-or-int 0;
2080
+
right = optional float-or-int 0;
2081
+
top = optional float-or-int 0;
2082
+
bottom = optional float-or-int 0;
2083
+
}
2084
+
// {
2085
+
description = ''
2086
+
The distances from the edges of the screen to the eges of the working area.
2087
+
2088
+
The top and bottom struts are absolute gaps from the edges of the screen. If you set a bottom strut of 64px and the scale is 2.0, then the output will have 128 physical pixels under the scrollable working area where it only shows the wallpaper.
2089
+
2090
+
Struts are computed in addition to layer-shell surfaces. If you have a waybar of 32px at the top, and you set a top strut of 16px, then you will have 48 logical pixels from the actual edge of the display to the top of the working area.
2091
+
2092
+
The left and right structs work in a similar way, except the padded space is not empty. The horizontal struts are used to constrain where focused windows are allowed to go. If you define a left strut of 64px and go to the first window in a workspace, that window will be aligned 64 logical pixels from the left edge of the output, rather than snapping to the actual edge of the screen. If another window exists to the left of this window, then you will see 64px of its right edge (if you have zero borders and gaps)
2093
+
'';
2094
+
};
2095
+
}
2096
+
];
2097
+
}
2098
+
2099
+
{
2100
+
animations =
2101
+
let
2102
+
animation-kind = types.attrTag {
2103
+
spring = section {
2104
+
damping-ratio = required types.float;
2105
+
stiffness = required types.int;
2106
+
epsilon = required types.float;
2107
+
};
2108
+
easing = section {
2109
+
duration-ms = required types.int;
2110
+
curve =
2111
+
required (enum [
2112
+
"linear"
2113
+
"ease-out-quad"
2114
+
"ease-out-cubic"
2115
+
"ease-out-expo"
2116
+
"cubic-bezier"
2117
+
])
2118
+
// {
2119
+
description = ''
2120
+
The curve to use for the easing function.
2121
+
'';
2122
+
};
2123
+
2124
+
# eh? not loving this. but anything better is kinda nontrivial.
2125
+
# will refactor, currently just a stopgap so that it is usable.
2126
+
curve-args = list kdl.types.kdl-value // {
2127
+
description = ''
2128
+
Arguments to the easing curve. ${fmt.code "cubic-bezier"} requires 4 arguments, all others don't allow arguments.
2129
+
'';
2130
+
};
2131
+
};
2132
+
};
2133
+
2134
+
anims = {
2135
+
workspace-switch.has-shader = false;
2136
+
horizontal-view-movement.has-shader = false;
2137
+
config-notification-open-close.has-shader = false;
2138
+
exit-confirmation-open-close.has-shader = false;
2139
+
window-movement.has-shader = false;
2140
+
window-open.has-shader = true;
2141
+
window-close.has-shader = true;
2142
+
window-resize.has-shader = true;
2143
+
screenshot-ui-open.has-shader = false;
2144
+
overview-open-close.has-shader = false;
2145
+
};
2146
+
in
2147
+
ordered-section [
2148
+
{
2149
+
enable = optional types.bool true;
2150
+
slowdown = nullable float-or-int;
2151
+
}
2152
+
{
2153
+
all-anims = mkOption {
2154
+
type = types.raw;
2155
+
internal = true;
2156
+
visible = false;
2157
+
2158
+
default = builtins.attrNames anims;
2159
+
};
2160
+
}
2161
+
(builtins.mapAttrs (
2162
+
name:
2163
+
(
2164
+
{ has-shader }:
2165
+
let
2166
+
inner = record (
2167
+
{
2168
+
enable = optional types.bool true;
2169
+
kind = nullable (shorthand-for "animation-kind" animation-kind) // {
2170
+
visible = "shallow";
2171
+
};
2172
+
}
2173
+
// lib.optionalAttrs has-shader {
2174
+
custom-shader = nullable types.str // {
2175
+
description = ''
2176
+
Source code for a GLSL shader to use for this animation.
2177
+
2178
+
For example, set it to ${fmt.code "builtins.readFile ./${name}.glsl"} to use a shader from the same directory as your configuration file.
2179
+
2180
+
See: ${fmt.bare-link "https://github.com/YaLTeR/niri/wiki/Configuration:-Animations#custom-shader"}
2181
+
'';
2182
+
};
2183
+
}
2184
+
);
2185
+
2186
+
actual-type = mkOptionType {
2187
+
inherit (inner)
2188
+
name
2189
+
description
2190
+
getSubOptions
2191
+
nestedTypes
2192
+
;
2193
+
2194
+
check = value: builtins.isNull value || animation-kind.check value || inner.check value;
2195
+
merge =
2196
+
loc: defs:
2197
+
inner.merge loc (
2198
+
map (
2199
+
def:
2200
+
if builtins.isNull def.value then
2201
+
lib.warn (obsolete-warning "${showOption loc} = null;" "${
2202
+
showOption (loc ++ [ "enable" ])
2203
+
} = false;" [ def ]) def
2204
+
// {
2205
+
value.enable = false;
2206
+
}
2207
+
else if animation-kind.check def.value then
2208
+
lib.warn (rename-warning loc (loc ++ [ "kind" ]) [ def ]) def // { value.kind = def.value; }
2209
+
else
2210
+
def
2211
+
) defs
2212
+
);
2213
+
};
2214
+
in
2215
+
optional actual-type { }
2216
+
)
2217
+
) anims)
2218
+
{
2219
+
"<animation-kind>" = docs-only animation-kind // {
2220
+
override-loc = lib.const [ "<animation-kind>" ];
2221
+
};
2222
+
}
2223
+
(
2224
+
let
2225
+
deprecated-shaders = [
2226
+
"window-open"
2227
+
"window-close"
2228
+
"window-resize"
2229
+
];
2230
+
in
2231
+
{
2232
+
__module =
2233
+
{
2234
+
options,
2235
+
config,
2236
+
...
2237
+
}:
2238
+
{
2239
+
options.shaders = lib.genAttrs deprecated-shaders (
2240
+
_: required (nullOr types.str) // { visible = false; }
2241
+
);
2242
+
config = lib.genAttrs deprecated-shaders (
2243
+
name:
2244
+
let
2245
+
old = options.shaders.${name};
2246
+
in
2247
+
lib.mkIf (old.isDefined) (
2248
+
lib.warn
2249
+
(rename-warning (old.loc) (options.${name}.loc ++ [ "custom-shader" ]) old.definitionsWithLocations)
2250
+
{
2251
+
custom-shader = config.shaders.${name};
2252
+
}
2253
+
)
2254
+
);
2255
+
};
2256
+
}
2257
+
)
2258
+
];
2259
+
2260
+
gestures =
2261
+
let
2262
+
scroll-description.trigger = measure: ''
2263
+
The ${measure} of the edge of the screen where dragging a window will scroll the view.
2264
+
'';
2265
+
scroll-description.delay-ms = ''
2266
+
The delay in milliseconds before the view starts scrolling.
2267
+
'';
2268
+
scroll-description.max-speed-for = measure: ''
2269
+
When the cursor is at boundary of the trigger ${measure}, the view will not be scrolling. Moving the mouse further away from the boundary and closer to the egde will linearly increase the scrolling speed, until the mouse is pressed against the edge of the screen, at which point the view will scroll at this speed. The speed is measured in logical pixels per second.
2270
+
'';
2271
+
in
2272
+
{
2273
+
dnd-edge-view-scroll =
2274
+
section {
2275
+
trigger-width = nullable float-or-int // {
2276
+
description = scroll-description.trigger "width";
2277
+
};
2278
+
delay-ms = nullable types.int // {
2279
+
description = scroll-description.delay-ms;
2280
+
};
2281
+
max-speed = nullable float-or-int // {
2282
+
description = scroll-description.max-speed-for "width";
2283
+
};
2284
+
}
2285
+
// {
2286
+
description = ''
2287
+
When dragging a window to the left or right edge of the screen, the view will start scrolling in that direction.
2288
+
'';
2289
+
};
2290
+
dnd-edge-workspace-switch =
2291
+
section {
2292
+
trigger-height = nullable float-or-int // {
2293
+
description = scroll-description.trigger "height";
2294
+
};
2295
+
delay-ms = nullable types.int // {
2296
+
description = scroll-description.delay-ms;
2297
+
};
2298
+
max-speed = nullable float-or-int // {
2299
+
description = scroll-description.max-speed-for "height";
2300
+
};
2301
+
}
2302
+
// {
2303
+
description = ''
2304
+
In the overview, when dragging a window to the top or bottom edge of the screen, view will start scrolling in that direction.
2305
+
2306
+
This does not happen when the overview is not open.
2307
+
'';
2308
+
};
2309
+
hot-corners.enable = optional types.bool true // {
2310
+
description = ''
2311
+
Put your mouse at the very top-left corner of a monitor to toggle the overview. Also works during drag-and-dropping something.
2312
+
'';
2313
+
};
2314
+
};
2315
+
}
2316
+
2317
+
{
2318
+
environment = attrs (nullOr types.str) // {
2319
+
description = ''
2320
+
Environment variables to set for processes spawned by niri.
2321
+
2322
+
If an environment variable is already set in the environment, then it will be overridden by the value set here.
2323
+
2324
+
If a value is null, then the environment variable will be unset, even if it already existed.
2325
+
2326
+
Examples:
2327
+
2328
+
${fmt.nix-code-block ''
2329
+
{
2330
+
${options.environment} = {
2331
+
QT_QPA_PLATFORM = "wayland";
2332
+
DISPLAY = null;
2333
+
};
2334
+
}
2335
+
''}
2336
+
'';
2337
+
};
2338
+
}
2339
+
2340
+
{
2341
+
window-rules =
2342
+
let
2343
+
window-rule-descriptions = rule-descriptions {
2344
+
surface = "window";
2345
+
surfaces = "windows";
2346
+
surface-rule = "window rule";
2347
+
Surface-rules = "Window rules";
2348
+
2349
+
self = options.window-rules;
2350
+
spawn-at-startup = options.spawn-at-startup;
2351
+
2352
+
example-fields = [
2353
+
''
2354
+
The ${fmt.code "title"} field, when non-null, is a regular expression. It will match a window if the client has set a title and its title matches the regular expression.
2355
+
''
2356
+
''
2357
+
The ${fmt.code "app-id"} field, when non-null, is a regular expression. It will match a window if the client has set an app id and its app id matches the regular expression.
2358
+
''
2359
+
];
2360
+
};
2361
+
2362
+
window-match = ordered-record' "match rule" [
2363
+
{
2364
+
app-id = nullable regex // {
2365
+
description = ''
2366
+
A regular expression to match against the app id of the window.
2367
+
2368
+
When non-null, for this field to match a window, a client must set the app id of its window and the app id must match this regex.
2369
+
'';
2370
+
};
2371
+
title = nullable regex // {
2372
+
description = ''
2373
+
A regular expression to match against the title of the window.
2374
+
2375
+
When non-null, for this field to match a window, a client must set the title of its window and the title must match this regex.
2376
+
'';
2377
+
};
2378
+
}
2379
+
{
2380
+
is-urgent = nullable types.bool // {
2381
+
description = ''
2382
+
When non-null, for this field to match a window, the value must match whether the window is in the urgent state or not.
2383
+
2384
+
A window can request attention by sending an XDG activation request. Such a request can be associated with an input event (e.g. in response to you clicking a notification), in which case it will be focused right away. It can also request attention without an input event, in which case it will simply be marked as "urgent". An urgent state doesn't do anything by itself, but it can be matched on to apply a window rule only to such windows.
2385
+
'';
2386
+
};
2387
+
is-active = nullable types.bool // {
2388
+
description = ''
2389
+
When non-null, for this field to match a window, the value must match whether the window is active or not.
2390
+
2391
+
Every monitor has up to one active window, and ${fmt.code "is-active=true"} will match the active window on each monitor. A monitor can have zero active windows if no windows are open on it. There can never be more than one active window on a monitor.
2392
+
'';
2393
+
};
2394
+
is-active-in-column = nullable types.bool // {
2395
+
description = ''
2396
+
When non-null, for this field to match a window, the value must match whether the window is active in its column or not.
2397
+
2398
+
Every column has exactly one active-in-column window. If it is the active column, this window is also the active window. A column may not have zero active-in-column windows, or more than one active-in-column window.
2399
+
2400
+
The active-in-column window is the window that was last focused in that column. When you switch focus to a column, the active-in-column window will be the new focused window.
2401
+
'';
2402
+
};
2403
+
is-focused = nullable types.bool // {
2404
+
description = ''
2405
+
When non-null, for this field to match a window, the value must match whether the window has keyboard focus or not.
2406
+
2407
+
A note on terminology used here: a window is actually a toplevel surface, and a surface just refers to any rectangular region that a client can draw to. A toplevel surface is just a surface with additional capabilities and properties (e.g. "fullscreen", "resizable", "min size", etc)
2408
+
2409
+
For a window to be focused, its surface must be focused. There is up to one focused surface, and it is the surface that can receive keyboard input. There can never be more than one focused surface. There can be zero focused surfaces if and only if there are zero surfaces. The focused surface does ${fmt.em "not"} have to be a toplevel surface. It can also be a layer-shell surface. In that case, there is a surface with keyboard focus but no ${fmt.em "window"} with keyboard focus.
2410
+
'';
2411
+
};
2412
+
is-floating = nullable types.bool // {
2413
+
description = ''
2414
+
When not-null, for this field to match a window, the value must match whether the window is floating (true) or tiled (false).
2415
+
'';
2416
+
};
2417
+
is-window-cast-target = nullable types.bool // {
2418
+
description = ''
2419
+
When non-null, matches based on whether the window is being targeted by a window cast.
2420
+
'';
2421
+
};
2422
+
}
2423
+
{
2424
+
at-startup = nullable types.bool // {
2425
+
description = window-rule-descriptions.match-at-startup;
2426
+
};
2427
+
}
2428
+
];
2429
+
in
2430
+
list (
2431
+
ordered-record' "window rule" [
2432
+
{
2433
+
matches = list window-match // {
2434
+
description = ''
2435
+
A list of rules to match windows.
2436
+
2437
+
If any of these rules match a window (or there are none), that window rule will be considered for this window. It can still be rejected by ${link-opt (subopts options.window-rules).excludes}
2438
+
2439
+
If all of the rules do not match a window, then this window rule will not apply to that window.
2440
+
'';
2441
+
};
2442
+
}
2443
+
{
2444
+
excludes = list window-match // {
2445
+
description = ''
2446
+
A list of rules to exclude windows.
2447
+
2448
+
If any of these rules match a window, then this window rule will not apply to that window, even if it matches one of the rules in ${link-opt (subopts options.window-rules).matches}
2449
+
2450
+
If none of these rules match a window, then this window rule will not be rejected. It will apply to that window if and only if it matches one of the rules in ${link-opt (subopts options.window-rules).matches}
2451
+
'';
2452
+
};
2453
+
}
2454
+
{
2455
+
default-column-width = nullable default-width // {
2456
+
description = ''
2457
+
The default width for new columns.
2458
+
2459
+
If the final value of this option is null, it default to ${link-opt (subopts options.layout).default-column-width}
2460
+
2461
+
If the final value option is not null, then its value will take priority over ${link-opt (subopts options.layout).default-column-width} for windows matching this rule.
2462
+
2463
+
An empty attrset ${fmt.code "{}"} is not the same as null. When this is set to an empty attrset ${fmt.code "{}"}, windows will get to decide their initial width. When set to null, it represents that this particular window rule has no effect on the default width (and it should instead be taken from an earlier rule or the global default).
2464
+
2465
+
'';
2466
+
};
2467
+
default-window-height = nullable default-height // {
2468
+
description = ''
2469
+
The default height for new floating windows.
2470
+
2471
+
This does nothing if the window is not floating when it is created.
2472
+
2473
+
There is no global default option for this in the layout section like for the column width. If the final value of this option is null, then it defaults to the empty attrset ${fmt.code "{}"}.
2474
+
2475
+
If this is set to an empty attrset ${fmt.code "{}"}, then it effectively "unsets" the default height for this window rule evaluation, as opposed to ${fmt.code "null"} which doesn't change the value at all. Future rules may still set it to a value and unset it again as they wish.
2476
+
2477
+
If the final value of this option is an empty attrset ${fmt.code "{}"}, then the client gets to decide the height of the window.
2478
+
2479
+
If the final value of this option is not an empty attrset ${fmt.code "{}"}, and the window spawns as floating, then the window will be created with the specified height.
2480
+
'';
2481
+
};
2482
+
default-column-display =
2483
+
nullable (enum [
2484
+
"normal"
2485
+
"tabbed"
2486
+
])
2487
+
// {
2488
+
description = ''
2489
+
When this window is inserted into the tiling layout such that a new column is created (e.g. when it is first opened, when it is expelled from an existing column, when it's moved to a new workspace, etc), this setting controls the default display mode of the column.
2490
+
2491
+
If the final value of this field is null, then the default display mode is taken from ${link-opt (subopts options.layout).default-column-display}.
2492
+
'';
2493
+
};
2494
+
}
2495
+
{
2496
+
open-on-output = nullable types.str // {
2497
+
description = ''
2498
+
The output to open this window on.
2499
+
2500
+
If final value of this field is an output that exists, the new window will open on that output.
2501
+
2502
+
If the final value is an output that does not exist, or it is null, then the window opens on the currently focused output.
2503
+
'';
2504
+
};
2505
+
open-on-workspace = nullable types.str // {
2506
+
description = ''
2507
+
The workspace to open this window on.
2508
+
2509
+
If the final value of this field is a named workspace that exists, the window will open on that workspace.
2510
+
2511
+
If the final value of this is a named workspace that does not exist, or it is null, the window opens on the currently focused workspace.
2512
+
'';
2513
+
};
2514
+
open-maximized = nullable types.bool // {
2515
+
description = ''
2516
+
Whether to open this window in a maximized column.
2517
+
2518
+
If the final value of this field is null or false, then the window will not open in a maximized column.
2519
+
2520
+
If the final value of this field is true, then the window will open in a maximized column.
2521
+
'';
2522
+
};
2523
+
open-fullscreen = nullable types.bool // {
2524
+
description = ''
2525
+
Whether to open this window in fullscreen.
2526
+
2527
+
If the final value of this field is true, then this window will always be forced to open in fullscreen.
2528
+
2529
+
If the final value of this field is false, then this window is never allowed to open in fullscreen, even if it requests to do so.
2530
+
2531
+
If the final value of this field is null, then the client gets to decide if this window will open in fullscreen.
2532
+
'';
2533
+
};
2534
+
open-floating = nullable types.bool // {
2535
+
description = ''
2536
+
Whether to open this window as floating.
2537
+
2538
+
If the final value of this field is true, then this window will always be forced to open as floating.
2539
+
2540
+
If the final value of this field is false, then this window is never allowed to open as floating.
2541
+
2542
+
If the final value of this field is null, then niri will decide whether to open the window as floating or as tiled.
2543
+
'';
2544
+
};
2545
+
2546
+
open-focused = nullable types.bool // {
2547
+
description = ''
2548
+
Whether to focus this window when it is opened.
2549
+
2550
+
If the final value of this field is null, then the window will be focused based on several factors:
2551
+
2552
+
${fmt.list [
2553
+
"If it provided a valid activation token that hasn't expired, it will be focused."
2554
+
"If the strict activation policy is enabled (not by default), the procedure ends here. It will be focused if and only if the activation token is valid."
2555
+
"Otherwise, if no valid activation token was presented, but the window is a dialog, it will open next to its parent and be focused anyways."
2556
+
"If the window is not a dialog, it will be focused if there is no fullscreen window; we don't want to steal its focus unless a dialog belongs to it."
2557
+
]}
2558
+
2559
+
(a dialog here means a toplevel surface that has a non-null parent)
2560
+
2561
+
If the final value of this field is not null, all of the above is ignored. Whether the window provides an activation token or not, doesn't matter. The window will be focused if and only if this field is true. If it is false, the window will not be focused, even if it provides a valid activation token.
2562
+
'';
2563
+
};
2564
+
}
2565
+
{
2566
+
block-out-from =
2567
+
nullable (enum [
2568
+
"screencast"
2569
+
"screen-capture"
2570
+
])
2571
+
// {
2572
+
description = window-rule-descriptions.block-out-from;
2573
+
};
2574
+
2575
+
geometry-corner-radius = geometry-corner-radius-rule // {
2576
+
description = ''
2577
+
The corner radii of the window decorations (border, focus ring, and shadow) in logical pixels.
2578
+
2579
+
By default, the actual window surface will be unaffected by this.
2580
+
2581
+
Set ${link-opt (subopts options.window-rules).clip-to-geometry} to true to clip the window to its visual geometry, i.e. apply the corner radius to the window surface itself.
2582
+
'';
2583
+
};
2584
+
2585
+
clip-to-geometry = nullable types.bool // {
2586
+
description = ''
2587
+
Whether to clip the window to its visual geometry, i.e. whether the corner radius should be applied to the window surface itself or just the decorations.
2588
+
'';
2589
+
};
2590
+
2591
+
border = border-rule {
2592
+
name = "border";
2593
+
window = "matched window";
2594
+
description = ''
2595
+
See ${link-opt (subopts options.layout).border}.
2596
+
'';
2597
+
};
2598
+
focus-ring = border-rule {
2599
+
name = "focus ring";
2600
+
window = "matched window with focus";
2601
+
description = ''
2602
+
See ${link-opt (subopts options.layout).focus-ring}.
2603
+
'';
2604
+
};
2605
+
2606
+
tab-indicator =
2607
+
let
2608
+
layout-tab-indicator = subopts (subopts options.layout).tab-indicator;
2609
+
in
2610
+
section' (
2611
+
{ options, ... }:
2612
+
{
2613
+
options = make-decoration-options options {
2614
+
urgent.description = ''
2615
+
See ${link-opt layout-tab-indicator.urgent}.
2616
+
'';
2617
+
active.description = ''
2618
+
See ${link-opt layout-tab-indicator.active}.
2619
+
'';
2620
+
inactive.description = ''
2621
+
See ${link-opt layout-tab-indicator.inactive}.
2622
+
'';
2623
+
};
2624
+
}
2625
+
);
2626
+
2627
+
shadow = shadow-rule;
2628
+
draw-border-with-background = nullable types.bool // {
2629
+
description = ''
2630
+
Whether to draw the focus ring and border with a background.
2631
+
2632
+
Normally, for windows with server-side decorations, niri will draw an actual border around them, because it knows they will be rectangular.
2633
+
2634
+
Because client-side decorations can take on arbitrary shapes, most notably including rounded corners, niri cannot really know the "correct" place to put a border, so for such windows it will draw a solid rectangle behind them instead.
2635
+
2636
+
For most windows, this looks okay. At worst, you have some uneven/jagged borders, instead of a gaping hole in the region outside of the corner radius of the window but inside its bounds.
2637
+
2638
+
If you wish to make windows sucha s your terminal transparent, and they use CSD, this is very undesirable. Instead of showing your wallpaper, you'll get a solid rectangle.
2639
+
2640
+
You can set this option per window to override niri's default behaviour, and instruct it to omit the border background for CSD windows. You can also explicitly enable it for SSD windows.
2641
+
'';
2642
+
};
2643
+
opacity = nullable types.float // {
2644
+
description = window-rule-descriptions.opacity;
2645
+
};
2646
+
}
2647
+
(
2648
+
let
2649
+
sizing-info = bound: ''
2650
+
Sets the ${bound} (in logical pixels) that niri will ever ask this window for.
2651
+
2652
+
Keep in mind that the window itself always has a final say in its size, and may not respect the ${bound} set by this option.
2653
+
'';
2654
+
2655
+
sizing-opt =
2656
+
bound:
2657
+
nullable types.int
2658
+
// {
2659
+
description = sizing-info bound;
2660
+
};
2661
+
in
2662
+
{
2663
+
min-width = sizing-opt "minimum width";
2664
+
max-width = sizing-opt "maximum width";
2665
+
min-height = sizing-opt "minimum height";
2666
+
max-height = nullable types.int // {
2667
+
description = ''
2668
+
${sizing-info "maximum height"}
2669
+
2670
+
Also, note that the maximum height is not taken into account when automatically sizing columns. That is, when a column is created normally, windows in it will be "automatically sized" to fill the vertical space. This algorithm will respect a minimum height, and not make windows any smaller than that, but the max height is only taken into account if it is equal to the min height. In other words, it will only accept a "fixed height" or a "minimum height". In practice, most windows do not set a max size unless it is equal to their min size, so this is usually not a problem without window rules.
2671
+
2672
+
If you manually change the window heights, then max-height will be taken into account and restrict you from making it any taller, as you'd intuitively expect.
2673
+
'';
2674
+
};
2675
+
}
2676
+
)
2677
+
{
2678
+
baba-is-float = nullable types.bool // {
2679
+
description = ''
2680
+
Makes your window FLOAT up and down, like in the game Baba Is You.
2681
+
2682
+
Made for April Fools 2025.
2683
+
'';
2684
+
};
2685
+
default-floating-position =
2686
+
nullable (record {
2687
+
x = required float-or-int;
2688
+
y = required float-or-int;
2689
+
relative-to = required (enum [
2690
+
"top-left"
2691
+
"top-right"
2692
+
"bottom-left"
2693
+
"bottom-right"
2694
+
"top"
2695
+
"bottom"
2696
+
"left"
2697
+
"right"
2698
+
]);
2699
+
})
2700
+
// {
2701
+
description = ''
2702
+
The default position for this window when it enters the floating layout.
2703
+
2704
+
If a window is created as floating, it will be placed at this position.
2705
+
2706
+
If a window is created as tiling, then later made floating, it will be placed at this position.
2707
+
2708
+
If a window has already been placed as floating through one of the above methods, and moved back to the tiling layout, then this option has no effect the next time it enters the floating layout. It will be placed at the same position it was last time.
2709
+
2710
+
The ${fmt.code "x"} and ${fmt.code "y"} fields are the distances from the edge of the screen to the edge of the window, in logical pixels. The ${fmt.code "relative-to"} field determines which two edges of the window and screen that these distances are measured from.
2711
+
'';
2712
+
};
2713
+
}
2714
+
{
2715
+
variable-refresh-rate = nullable types.bool // {
2716
+
description = ''
2717
+
Takes effect only when the window is on an output with ${link-opt (subopts options.outputs).variable-refresh-rate} set to ${fmt.code ''"on-demand"''}. If the final value of this field is true, then the output will enable variable refresh rate when this window is present on it.
2718
+
'';
2719
+
};
2720
+
}
2721
+
{
2722
+
scroll-factor = nullable float-or-int;
2723
+
}
2724
+
{
2725
+
tiled-state = nullable types.bool;
2726
+
}
2727
+
]
2728
+
)
2729
+
// {
2730
+
description = window-rule-descriptions.top-option;
2731
+
};
2732
+
}
2733
+
2734
+
{
2735
+
layer-rules =
2736
+
let
2737
+
layer-rule-descriptions = rule-descriptions {
2738
+
surface = "layer surface";
2739
+
surfaces = "layer surfaces";
2740
+
surface-rule = "layer rule";
2741
+
Surface-rules = "Layer rules";
2742
+
2743
+
self = options.layer-rules;
2744
+
spawn-at-startup = options.spawn-at-startup;
2745
+
2746
+
example-fields = [
2747
+
''
2748
+
The ${fmt.code "namespace"} field, when non-null, is a regular expression. It will match a layer surface for which the client has set a namespace that matches the regular expression.
2749
+
''
2750
+
];
2751
+
};
2752
+
2753
+
layer-match = ordered-record' "match rule" [
2754
+
{
2755
+
namespace = nullable regex // {
2756
+
description = ''
2757
+
A regular expression to match against the namespace of the layer surface.
2758
+
2759
+
All layer surfaces have a namespace set once at creation. When this rule is non-null, the regex must match the namespace of the layer surface for this rule to match.
2760
+
'';
2761
+
};
2762
+
}
2763
+
{
2764
+
at-startup = nullable types.bool // {
2765
+
description = layer-rule-descriptions.match-at-startup;
2766
+
};
2767
+
}
2768
+
];
2769
+
in
2770
+
list (
2771
+
ordered-record' "layer rule" [
2772
+
{
2773
+
matches = list layer-match // {
2774
+
description = layer-rule-descriptions.match;
2775
+
};
2776
+
}
2777
+
{
2778
+
excludes = list layer-match // {
2779
+
description = layer-rule-descriptions.exclude;
2780
+
};
2781
+
}
2782
+
{
2783
+
block-out-from =
2784
+
nullable (enum [
2785
+
"screencast"
2786
+
"screen-capture"
2787
+
])
2788
+
// {
2789
+
description = layer-rule-descriptions.block-out-from;
2790
+
};
2791
+
2792
+
opacity = nullable types.float // {
2793
+
description = layer-rule-descriptions.opacity;
2794
+
};
2795
+
}
2796
+
{
2797
+
shadow = shadow-rule;
2798
+
geometry-corner-radius = geometry-corner-radius-rule // {
2799
+
description = ''
2800
+
The corner radii of the surface decorations (shadow) in logical pixels.
2801
+
'';
2802
+
};
2803
+
}
2804
+
{
2805
+
place-within-backdrop = nullable types.bool // {
2806
+
description = ''
2807
+
Set to ${fmt.code "true"} to place the surface into the backdrop visible in the Overview and between workspaces.
2808
+
This will only work for background layer surfaces that ignore exclusive zones (typical for wallpaper tools). Layers within the backdrop will ignore all input.
2809
+
'';
2810
+
};
2811
+
2812
+
baba-is-float = nullable types.bool // {
2813
+
description = ''
2814
+
Make your layer surfaces FLOAT up and down.
2815
+
2816
+
This is a natural extension of the April Fools' 2025 feature.
2817
+
'';
2818
+
};
2819
+
}
2820
+
]
2821
+
)
2822
+
// {
2823
+
description = layer-rule-descriptions.top-option;
2824
+
};
2825
+
}
2826
+
2827
+
{
2828
+
xwayland-satellite =
2829
+
section {
2830
+
enable = optional types.bool true;
2831
+
path = nullable types.str // {
2832
+
description = ''
2833
+
Path to the xwayland-satellite binary.
2834
+
2835
+
Set it to something like ${fmt.code "lib.getExe pkgs.xwayland-satellite-unstable"}.
2836
+
'';
2837
+
};
2838
+
}
2839
+
// {
2840
+
description = ''
2841
+
Xwayland-satellite integration. Requires unstable niri and unstable xwayland-satellite.
2842
+
'';
2843
+
};
2844
+
}
2845
+
2846
+
{
2847
+
debug = attrs kdl.types.kdl-args // {
2848
+
description = ''
2849
+
Debug options for niri.
2850
+
2851
+
${fmt.code "kdl arguments"} in the type refers to a list of arguments passed to a node under the ${fmt.code "debug"} section. This is a way to pass arbitrary KDL-valid data to niri. See ${link-opt (subopts options.binds).action} for more information on all the ways you can use this.
2852
+
2853
+
Note that for no-argument nodes, there is no special way to define them here. You can't pass them as just a "string" because that makes no sense here. You must pass it an empty array of arguments.
2854
+
2855
+
Here's an example of how to use this:
2856
+
2857
+
${fmt.nix-code-block ''
2858
+
{
2859
+
${options.debug} = {
2860
+
disable-cursor-plane = [];
2861
+
render-drm-device = "/dev/dri/renderD129";
2862
+
};
2863
+
}
2864
+
''}
2865
+
2866
+
This option is, just like ${link-opt (subopts options.binds).action}, not verified by the nix module. But, it will be validated by niri before committing the config.
2867
+
2868
+
Additionally, i don't guarantee stability of the debug options. They may change at any time without prior notice, either because of niri changing the available options, or because of me changing this to a more reasonable schema.
2869
+
'';
2870
+
};
2871
+
}
2872
+
];
2873
+
}
2874
+
);
2875
+
in
2876
+
{
2877
+
niri-type = type-with docs.settings-fmt;
2878
+
2879
+
niri-render =
2880
+
cfg:
2881
+
if cfg == null then
2882
+
null
2883
+
else
2884
+
let
2885
+
normalize-nodes = nodes: lib.remove null (lib.flatten nodes);
2886
+
2887
+
node =
2888
+
name: args: children:
2889
+
kdl.node name args (normalize-nodes children);
2890
+
plain = name: node name [ ];
2891
+
leaf = name: args: node name args [ ];
2892
+
flag = name: node name [ ] [ ];
2893
+
2894
+
optional-node = cond: v: if cond then v else null;
2895
+
2896
+
nullable =
2897
+
f: name: value:
2898
+
optional-node (value != null) (f name value);
2899
+
flag' = name: lib.flip optional-node (flag name);
2900
+
plain' =
2901
+
name: children:
2902
+
optional-node (builtins.any (v: v != null) (lib.flatten children)) (plain name children);
2903
+
2904
+
map' =
2905
+
node: f: name: val:
2906
+
node name (f val);
2907
+
2908
+
each = list: f: map f list;
2909
+
each' = attrs: each (builtins.attrValues attrs);
2910
+
2911
+
toggle =
2912
+
disabled: cfg: contents:
2913
+
if cfg.enable then contents else flag disabled;
2914
+
2915
+
toggle' = disabled: cfg: contents: [
2916
+
(flag' disabled (cfg.enable == false))
2917
+
contents
2918
+
];
2919
+
2920
+
pointer = cfg: [
2921
+
(flag' "natural-scroll" cfg.natural-scroll)
2922
+
(flag' "middle-emulation" cfg.middle-emulation)
2923
+
(nullable leaf "accel-speed" cfg.accel-speed)
2924
+
(nullable leaf "accel-profile" cfg.accel-profile)
2925
+
(nullable leaf "scroll-button" cfg.scroll-button)
2926
+
(flag' "scroll-button-lock" cfg.scroll-button-lock)
2927
+
(nullable leaf "scroll-method" cfg.scroll-method)
2928
+
];
2929
+
2930
+
pointer-tablet =
2931
+
cfg: inner:
2932
+
(toggle "off" cfg [
2933
+
(flag' "left-handed" cfg.left-handed)
2934
+
inner
2935
+
]);
2936
+
2937
+
touchy = cfg: [
2938
+
(nullable leaf "map-to-output" cfg.map-to-output)
2939
+
];
2940
+
2941
+
tablet =
2942
+
cfg:
2943
+
touchy cfg
2944
+
++ [
2945
+
(nullable leaf "calibration-matrix" cfg.calibration-matrix)
2946
+
];
2947
+
2948
+
touch =
2949
+
cfg:
2950
+
(toggle "off" cfg [
2951
+
(touchy cfg)
2952
+
]);
2953
+
2954
+
gradient' =
2955
+
name: cfg:
2956
+
leaf name (
2957
+
lib.concatMapAttrs (
2958
+
name: value:
2959
+
lib.optionalAttrs (value != null) {
2960
+
${lib.removeSuffix "'" name} = value;
2961
+
}
2962
+
) cfg
2963
+
);
2964
+
2965
+
borderish = map' plain (
2966
+
cfg:
2967
+
toggle "off" cfg [
2968
+
(leaf "width" cfg.width)
2969
+
(nullable leaf "urgent-color" cfg.urgent.color or null)
2970
+
(nullable gradient' "urgent-gradient" cfg.urgent.gradient or null)
2971
+
(nullable leaf "active-color" cfg.active.color or null)
2972
+
(nullable gradient' "active-gradient" cfg.active.gradient or null)
2973
+
(nullable leaf "inactive-color" cfg.inactive.color or null)
2974
+
(nullable gradient' "inactive-gradient" cfg.inactive.gradient or null)
2975
+
]
2976
+
);
2977
+
2978
+
shadow = map' (nullable plain) (
2979
+
cfg:
2980
+
optional-node (cfg.enable) [
2981
+
(flag "on")
2982
+
(leaf "offset" cfg.offset)
2983
+
(leaf "softness" cfg.softness)
2984
+
(leaf "spread" cfg.spread)
2985
+
2986
+
(leaf "draw-behind-window" cfg.draw-behind-window)
2987
+
(leaf "color" cfg.color)
2988
+
(nullable leaf "inactive-color" cfg.inactive-color)
2989
+
]
2990
+
);
2991
+
2992
+
tab-indicator = map' plain (
2993
+
cfg:
2994
+
toggle "off" cfg [
2995
+
(flag' "hide-when-single-tab" cfg.hide-when-single-tab)
2996
+
(flag' "place-within-column" cfg.place-within-column)
2997
+
(leaf "gap" cfg.gap)
2998
+
(leaf "width" cfg.width)
2999
+
(leaf "length" cfg.length)
3000
+
(leaf "position" cfg.position)
3001
+
(leaf "gaps-between-tabs" cfg.gaps-between-tabs)
3002
+
(leaf "corner-radius" cfg.corner-radius)
3003
+
(nullable leaf "urgent-color" cfg.urgent.color or null)
3004
+
(nullable gradient' "urgent-gradient" cfg.urgent.gradient or null)
3005
+
(nullable leaf "active-color" cfg.active.color or null)
3006
+
(nullable gradient' "active-gradient" cfg.active.gradient or null)
3007
+
(nullable leaf "inactive-color" cfg.inactive.color or null)
3008
+
(nullable gradient' "inactive-gradient" cfg.inactive.gradient or null)
3009
+
]
3010
+
);
3011
+
3012
+
preset-sizes = map' (nullable plain) (
3013
+
cfg: if cfg == [ ] then null else map (lib.mapAttrsToList leaf) (lib.toList cfg)
3014
+
);
3015
+
3016
+
animation = map' plain' (
3017
+
cfg:
3018
+
toggle "off" cfg [
3019
+
(optional-node (cfg.kind ? easing) [
3020
+
(leaf "duration-ms" cfg.kind.easing.duration-ms)
3021
+
(leaf "curve" ([ cfg.kind.easing.curve ] ++ cfg.kind.easing.curve-args))
3022
+
])
3023
+
(nullable leaf "spring" cfg.kind.spring or null)
3024
+
(nullable leaf "custom-shader" cfg.custom-shader or null)
3025
+
]
3026
+
);
3027
+
3028
+
opt-props = lib.filterAttrs (lib.const (value: value != null));
3029
+
border-rule = map' plain' (cfg: [
3030
+
(flag' "on" (cfg.enable == true))
3031
+
(flag' "off" (cfg.enable == false))
3032
+
(nullable leaf "width" cfg.width)
3033
+
(nullable leaf "urgent-color" cfg.urgent.color or null)
3034
+
(nullable gradient' "urgent-gradient" cfg.urgent.gradient or null)
3035
+
(nullable leaf "active-color" cfg.active.color or null)
3036
+
(nullable gradient' "active-gradient" cfg.active.gradient or null)
3037
+
(nullable leaf "inactive-color" cfg.inactive.color or null)
3038
+
(nullable gradient' "inactive-gradient" cfg.inactive.gradient or null)
3039
+
]);
3040
+
3041
+
shadow-rule = map' plain' (cfg: [
3042
+
(flag' "on" (cfg.enable == true))
3043
+
(flag' "off" (cfg.enable == false))
3044
+
(nullable leaf "offset" cfg.offset)
3045
+
(nullable leaf "softness" cfg.softness)
3046
+
(nullable leaf "spread" cfg.spread)
3047
+
(nullable leaf "draw-behind-window" cfg.draw-behind-window)
3048
+
(nullable leaf "color" cfg.color)
3049
+
(nullable leaf "inactive-color" cfg.inactive-color)
3050
+
]);
3051
+
3052
+
tab-indicator-rule = map' plain' (cfg: [
3053
+
(nullable leaf "urgent-color" cfg.urgent.color or null)
3054
+
(nullable gradient' "urgent-gradient" cfg.urgent.gradient or null)
3055
+
(nullable leaf "active-color" cfg.active.color or null)
3056
+
(nullable gradient' "active-gradient" cfg.active.gradient or null)
3057
+
(nullable leaf "inactive-color" cfg.inactive.color or null)
3058
+
(nullable gradient' "inactive-gradient" cfg.inactive.gradient or null)
3059
+
]);
3060
+
3061
+
corner-radius = cfg: [
3062
+
cfg.top-left
3063
+
cfg.top-right
3064
+
cfg.bottom-right
3065
+
cfg.bottom-left
3066
+
];
3067
+
3068
+
transform =
3069
+
cfg:
3070
+
let
3071
+
rotation = toString cfg.rotation;
3072
+
basic = if cfg.flipped then "flipped-${rotation}" else "${rotation}";
3073
+
replacement."0" = "normal";
3074
+
replacement."flipped-0" = "flipped";
3075
+
in
3076
+
replacement.${basic} or basic;
3077
+
3078
+
mode =
3079
+
cfg:
3080
+
let
3081
+
cfg' = builtins.mapAttrs (lib.const toString) cfg;
3082
+
in
3083
+
if cfg.refresh == null then
3084
+
"${cfg'.width}x${cfg'.height}"
3085
+
else
3086
+
"${cfg'.width}x${cfg'.height}@${cfg'.refresh}";
3087
+
3088
+
bind =
3089
+
name: cfg:
3090
+
let
3091
+
bool-props-with-defaults =
3092
+
cfg: defaults:
3093
+
opt-props (
3094
+
builtins.mapAttrs (
3095
+
name: value: (if (defaults ? ${name}) && (value != defaults.${name}) then value else null)
3096
+
) cfg
3097
+
);
3098
+
in
3099
+
node name
3100
+
(
3101
+
opt-props {
3102
+
inherit (cfg) cooldown-ms;
3103
+
}
3104
+
// bool-props-with-defaults cfg {
3105
+
repeat = true;
3106
+
allow-when-locked = false;
3107
+
allow-inhibiting = true;
3108
+
}
3109
+
// lib.optionalAttrs (cfg.hotkey-overlay.hidden or false) {
3110
+
hotkey-overlay-title = null;
3111
+
}
3112
+
// opt-props {
3113
+
hotkey-overlay-title = cfg.hotkey-overlay.title or null;
3114
+
}
3115
+
)
3116
+
[
3117
+
(lib.mapAttrsToList leaf cfg.action)
3118
+
];
3119
+
3120
+
pointer-tablet' =
3121
+
ext: name: cfg:
3122
+
plain' name (pointer-tablet cfg (ext cfg));
3123
+
pointer' = pointer-tablet' pointer;
3124
+
tablet' = pointer-tablet' tablet;
3125
+
in
3126
+
normalize-nodes [
3127
+
(plain "input" [
3128
+
(plain "keyboard" [
3129
+
(plain "xkb" [
3130
+
(nullable leaf "file" cfg.input.keyboard.xkb.file)
3131
+
(leaf "layout" cfg.input.keyboard.xkb.layout)
3132
+
(leaf "model" cfg.input.keyboard.xkb.model)
3133
+
(leaf "rules" cfg.input.keyboard.xkb.rules)
3134
+
(leaf "variant" cfg.input.keyboard.xkb.variant)
3135
+
(nullable leaf "options" cfg.input.keyboard.xkb.options)
3136
+
])
3137
+
(leaf "repeat-delay" cfg.input.keyboard.repeat-delay)
3138
+
(leaf "repeat-rate" cfg.input.keyboard.repeat-rate)
3139
+
(leaf "track-layout" cfg.input.keyboard.track-layout)
3140
+
(flag' "numlock" cfg.input.keyboard.numlock)
3141
+
])
3142
+
(plain' "touchpad" (
3143
+
pointer-tablet cfg.input.touchpad [
3144
+
(flag' "tap" cfg.input.touchpad.tap)
3145
+
(flag' "dwt" cfg.input.touchpad.dwt)
3146
+
(flag' "dwtp" cfg.input.touchpad.dwtp)
3147
+
(nullable leaf "drag" cfg.input.touchpad.drag)
3148
+
(flag' "drag-lock" cfg.input.touchpad.drag-lock)
3149
+
(flag' "disabled-on-external-mouse" cfg.input.touchpad.disabled-on-external-mouse)
3150
+
(pointer cfg.input.touchpad)
3151
+
(nullable leaf "click-method" cfg.input.touchpad.click-method)
3152
+
(nullable leaf "tap-button-map" cfg.input.touchpad.tap-button-map)
3153
+
(nullable leaf "scroll-factor" cfg.input.touchpad.scroll-factor)
3154
+
]
3155
+
))
3156
+
(plain' "mouse" (
3157
+
pointer-tablet cfg.input.mouse [
3158
+
(pointer cfg.input.mouse)
3159
+
(nullable leaf "scroll-factor" cfg.input.mouse.scroll-factor)
3160
+
]
3161
+
))
3162
+
(pointer' "trackpoint" cfg.input.trackpoint)
3163
+
(pointer' "trackball" cfg.input.trackball)
3164
+
(tablet' "tablet" cfg.input.tablet)
3165
+
(plain' "touch" (touch cfg.input.touch))
3166
+
(optional-node cfg.input.warp-mouse-to-focus.enable (
3167
+
leaf "warp-mouse-to-focus" (
3168
+
lib.optionalAttrs (cfg.input.warp-mouse-to-focus.mode != null) {
3169
+
inherit (cfg.input.warp-mouse-to-focus) mode;
3170
+
}
3171
+
)
3172
+
))
3173
+
(optional-node cfg.input.focus-follows-mouse.enable (
3174
+
leaf "focus-follows-mouse" (
3175
+
lib.optionalAttrs (cfg.input.focus-follows-mouse.max-scroll-amount != null) {
3176
+
inherit (cfg.input.focus-follows-mouse) max-scroll-amount;
3177
+
}
3178
+
)
3179
+
))
3180
+
(flag' "workspace-auto-back-and-forth" cfg.input.workspace-auto-back-and-forth)
3181
+
(toggle "disable-power-key-handling" cfg.input.power-key-handling [ ])
3182
+
(nullable leaf "mod-key" cfg.input.mod-key)
3183
+
(nullable leaf "mod-key-nested" cfg.input.mod-key-nested)
3184
+
])
3185
+
3186
+
(each' cfg.outputs (cfg: [
3187
+
(node "output" cfg.name [
3188
+
(toggle' "off" cfg [
3189
+
(nullable leaf "backdrop-color" cfg.backdrop-color)
3190
+
(nullable leaf "background-color" cfg.background-color)
3191
+
(nullable leaf "scale" cfg.scale)
3192
+
(flag' "focus-at-startup" cfg.focus-at-startup)
3193
+
(map' leaf transform "transform" cfg.transform)
3194
+
(nullable leaf "position" cfg.position)
3195
+
(nullable (map' leaf mode) "mode" cfg.mode)
3196
+
(optional-node (cfg.variable-refresh-rate != false) (
3197
+
leaf "variable-refresh-rate" { on-demand = cfg.variable-refresh-rate == "on-demand"; }
3198
+
))
3199
+
])
3200
+
])
3201
+
]))
3202
+
3203
+
(leaf "screenshot-path" cfg.screenshot-path)
3204
+
(flag' "prefer-no-csd" cfg.prefer-no-csd)
3205
+
3206
+
(plain' "overview" [
3207
+
(nullable leaf "zoom" cfg.overview.zoom)
3208
+
(nullable leaf "backdrop-color" cfg.overview.backdrop-color)
3209
+
(plain' "workspace-shadow" [
3210
+
(toggle "off" cfg.overview.workspace-shadow [
3211
+
(nullable leaf "offset" cfg.overview.workspace-shadow.offset)
3212
+
(nullable leaf "softness" cfg.overview.workspace-shadow.softness)
3213
+
(nullable leaf "spread" cfg.overview.workspace-shadow.spread)
3214
+
(nullable leaf "color" cfg.overview.workspace-shadow.color)
3215
+
])
3216
+
])
3217
+
])
3218
+
3219
+
(plain "layout" [
3220
+
(leaf "gaps" cfg.layout.gaps)
3221
+
(plain "struts" [
3222
+
(leaf "left" cfg.layout.struts.left)
3223
+
(leaf "right" cfg.layout.struts.right)
3224
+
(leaf "top" cfg.layout.struts.top)
3225
+
(leaf "bottom" cfg.layout.struts.bottom)
3226
+
])
3227
+
(borderish "focus-ring" cfg.layout.focus-ring)
3228
+
(borderish "border" cfg.layout.border)
3229
+
(nullable leaf "background-color" cfg.layout.background-color)
3230
+
(shadow "shadow" cfg.layout.shadow)
3231
+
(nullable tab-indicator "tab-indicator" cfg.layout.tab-indicator)
3232
+
(plain' "insert-hint" [
3233
+
(toggle "off" cfg.layout.insert-hint [
3234
+
(nullable leaf "color" cfg.layout.insert-hint.display.color or null)
3235
+
(nullable gradient' "gradient" cfg.layout.insert-hint.display.gradient or null)
3236
+
])
3237
+
])
3238
+
(preset-sizes "default-column-width" cfg.layout.default-column-width)
3239
+
(preset-sizes "preset-column-widths" cfg.layout.preset-column-widths)
3240
+
(preset-sizes "preset-window-heights" cfg.layout.preset-window-heights)
3241
+
(leaf "center-focused-column" cfg.layout.center-focused-column)
3242
+
(optional-node (cfg.layout.default-column-display != "normal") (
3243
+
leaf "default-column-display" cfg.layout.default-column-display
3244
+
))
3245
+
(flag' "always-center-single-column" cfg.layout.always-center-single-column)
3246
+
(flag' "empty-workspace-above-first" cfg.layout.empty-workspace-above-first)
3247
+
])
3248
+
3249
+
(plain "cursor" [
3250
+
(leaf "xcursor-theme" cfg.cursor.theme)
3251
+
(leaf "xcursor-size" cfg.cursor.size)
3252
+
(flag' "hide-when-typing" cfg.cursor.hide-when-typing)
3253
+
(nullable leaf "hide-after-inactive-ms" cfg.cursor.hide-after-inactive-ms)
3254
+
])
3255
+
3256
+
(plain' "hotkey-overlay" [
3257
+
(flag' "skip-at-startup" cfg.hotkey-overlay.skip-at-startup)
3258
+
(flag' "hide-not-bound" cfg.hotkey-overlay.hide-not-bound)
3259
+
])
3260
+
3261
+
(plain' "config-notification" [
3262
+
(flag' "disable-failed" cfg.config-notification.disable-failed)
3263
+
])
3264
+
3265
+
(plain' "clipboard" [
3266
+
(flag' "disable-primary" cfg.clipboard.disable-primary)
3267
+
])
3268
+
3269
+
(plain' "environment" (lib.mapAttrsToList leaf cfg.environment))
3270
+
(plain' "binds" (lib.mapAttrsToList bind cfg.binds))
3271
+
3272
+
(plain' "switch-events" (
3273
+
lib.mapAttrsToList (nullable (
3274
+
map' plain (cfg: [
3275
+
(lib.mapAttrsToList leaf cfg.action)
3276
+
])
3277
+
)) cfg.switch-events
3278
+
))
3279
+
3280
+
(each' cfg.workspaces (cfg: [
3281
+
(node "workspace" cfg.name [
3282
+
(nullable leaf "open-on-output" cfg.open-on-output)
3283
+
])
3284
+
]))
3285
+
3286
+
(each cfg.spawn-at-startup (cfg: [
3287
+
(nullable leaf "spawn-at-startup" cfg.argv or null)
3288
+
(nullable leaf "spawn-sh-at-startup" cfg.sh or null)
3289
+
(nullable leaf "spawn-at-startup" cfg.command or null)
3290
+
]))
3291
+
3292
+
(each cfg.window-rules (cfg: [
3293
+
(plain "window-rule" [
3294
+
(map (leaf "match") (map opt-props cfg.matches))
3295
+
(map (leaf "exclude") (map opt-props cfg.excludes))
3296
+
(nullable preset-sizes "default-column-width" cfg.default-column-width)
3297
+
(nullable preset-sizes "default-window-height" cfg.default-window-height)
3298
+
(nullable leaf "default-column-display" cfg.default-column-display)
3299
+
(nullable leaf "open-on-output" cfg.open-on-output)
3300
+
(nullable leaf "open-on-workspace" cfg.open-on-workspace)
3301
+
(nullable leaf "open-maximized" cfg.open-maximized)
3302
+
(nullable leaf "open-fullscreen" cfg.open-fullscreen)
3303
+
(nullable leaf "open-floating" cfg.open-floating)
3304
+
(nullable leaf "open-focused" cfg.open-focused)
3305
+
(nullable leaf "draw-border-with-background" cfg.draw-border-with-background)
3306
+
(nullable (map' leaf corner-radius) "geometry-corner-radius" cfg.geometry-corner-radius)
3307
+
(nullable leaf "clip-to-geometry" cfg.clip-to-geometry)
3308
+
(border-rule "border" cfg.border)
3309
+
(border-rule "focus-ring" cfg.focus-ring)
3310
+
(shadow-rule "shadow" cfg.shadow)
3311
+
(tab-indicator-rule "tab-indicator" cfg.tab-indicator)
3312
+
(nullable leaf "opacity" cfg.opacity)
3313
+
(nullable leaf "min-width" cfg.min-width)
3314
+
(nullable leaf "max-width" cfg.max-width)
3315
+
(nullable leaf "min-height" cfg.min-height)
3316
+
(nullable leaf "max-height" cfg.max-height)
3317
+
(nullable leaf "block-out-from" cfg.block-out-from)
3318
+
(nullable leaf "baba-is-float" cfg.baba-is-float)
3319
+
(nullable leaf "default-floating-position" cfg.default-floating-position)
3320
+
(nullable leaf "variable-refresh-rate" cfg.variable-refresh-rate)
3321
+
(nullable leaf "scroll-factor" cfg.scroll-factor)
3322
+
(nullable leaf "tiled-state" cfg.tiled-state)
3323
+
])
3324
+
]))
3325
+
(each cfg.layer-rules (cfg: [
3326
+
(plain "layer-rule" [
3327
+
(map (leaf "match") (map opt-props cfg.matches))
3328
+
(map (leaf "exclude") (map opt-props cfg.excludes))
3329
+
(nullable leaf "opacity" cfg.opacity)
3330
+
(nullable leaf "block-out-from" cfg.block-out-from)
3331
+
(shadow-rule "shadow" cfg.shadow)
3332
+
(nullable (map' leaf corner-radius) "geometry-corner-radius" cfg.geometry-corner-radius)
3333
+
(nullable leaf "place-within-backdrop" cfg.place-within-backdrop)
3334
+
(nullable leaf "baba-is-float" cfg.baba-is-float)
3335
+
])
3336
+
]))
3337
+
3338
+
(plain' "gestures" [
3339
+
(plain' "dnd-edge-view-scroll" [
3340
+
(nullable leaf "trigger-width" cfg.gestures.dnd-edge-view-scroll.trigger-width)
3341
+
(nullable leaf "delay-ms" cfg.gestures.dnd-edge-view-scroll.delay-ms)
3342
+
(nullable leaf "max-speed" cfg.gestures.dnd-edge-view-scroll.max-speed)
3343
+
])
3344
+
(plain' "dnd-edge-workspace-switch" [
3345
+
(nullable leaf "trigger-height" cfg.gestures.dnd-edge-workspace-switch.trigger-height)
3346
+
(nullable leaf "delay-ms" cfg.gestures.dnd-edge-workspace-switch.delay-ms)
3347
+
(nullable leaf "max-speed" cfg.gestures.dnd-edge-workspace-switch.max-speed)
3348
+
])
3349
+
(plain' "hot-corners" (toggle "off" cfg.gestures.hot-corners [ ]))
3350
+
])
3351
+
3352
+
(plain' "animations" [
3353
+
(toggle "off" cfg.animations [
3354
+
(nullable leaf "slowdown" cfg.animations.slowdown)
3355
+
(map (name: animation name cfg.animations.${name}) cfg.animations.all-anims)
3356
+
])
3357
+
])
3358
+
3359
+
(plain' "xwayland-satellite" [
3360
+
(toggle "off" cfg.xwayland-satellite [
3361
+
(nullable leaf "path" cfg.xwayland-satellite.path)
3362
+
])
3363
+
])
3364
+
3365
+
(map' plain' (lib.mapAttrsToList leaf) "debug" cfg.debug)
3366
+
];
3367
+
}