-
Notifications
You must be signed in to change notification settings - Fork 15
/
settings.nix
2507 lines (2168 loc) · 106 KB
/
settings.nix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{
inputs,
kdl,
lib,
docs,
binds,
settings,
...
}: {
type = let
inherit (lib) flip pipe showOption mkOption mkOptionType types;
inherit (lib.types) nullOr attrsOf listOf submodule enum;
binds-stable = binds inputs.niri-stable;
binds-unstable = binds inputs.niri-unstable;
record = opts: let
base = submodule (
if builtins.isFunction opts || (opts ? options && opts ? config)
then opts
else {options = opts;}
);
in
mkOptionType {
name = "record";
inherit (base) description check merge nestedTypes getSubOptions;
};
required = type: mkOption {inherit type;};
nullable = type: optional (nullOr type) null;
optional = type: default: mkOption {inherit type default;};
readonly = type: value: optional type value // {readOnly = true;};
attrs = type: optional (attrsOf type) {};
list = type: optional (listOf type) [];
float-or-int = types.either types.float types.int;
variant = variants:
mkOptionType {
name = "variant";
description =
if variants == {}
then "impossible (empty variant)"
else "variant of: ${builtins.concatStringsSep " | " (builtins.attrNames variants)}";
descriptionClass =
if variants == {}
then "noun"
else "composite";
check = v: let
names = builtins.attrNames v;
name = builtins.head names;
in
builtins.isAttrs v && builtins.length names == 1 && builtins.elem name (builtins.attrNames variants) && variants.${name}.check v.${name};
merge = loc: definitions: let
defs-for = name:
pipe definitions [
(builtins.filter (lib.hasAttrByPath ["value" name]))
(map (def: def // {value = def.value.${name};}))
];
merged = builtins.mapAttrs (name: type:
type.merge (loc ++ [name]) (defs-for name))
(lib.filterAttrs (name: _type: defs-for name != []) variants);
in
if merged == {}
then throw "The option `${showOption loc}` has no definitions, but one is required"
else if builtins.length (builtins.attrNames merged) == 1
then merged
else throw "The option `${showOption loc}` has conflicting definitions of multiple variants";
nestedTypes = variants;
inherit
(record (builtins.mapAttrs (lib.const (type:
(required type)
// (lib.optionalAttrs (type ? variant-description) {
description = type.variant-description;
})))
variants))
getSubOptions
;
};
inherit (docs.lib) libinput-link libinput-doc link-niri-release link-this-github link' anchor' unstable-note;
basic-pointer = default-natural-scroll: {
natural-scroll =
optional types.bool default-natural-scroll
// {
description = ''
Whether scrolling should move the content in the scrolled direction (as opposed to moving the viewport)
Further reading:
- ${libinput-link "configuration" "Scrolling"}
- ${libinput-link "scrolling" "Natural scrolling vs. traditional scrolling"}
'';
};
middle-emulation =
optional types.bool false
// {
description = ''
Whether a middle mouse button press should be sent when you press the left and right mouse buttons
Further reading:
- ${libinput-link "configuration" "Middle Button Emulation"}
- ${libinput-link "middle-button-emulation" "Middle button emulation"}
'';
};
accel-speed =
optional types.float 0.0
// {
description = ''
Further reading:
- ${libinput-link "configuration" "Pointer acceleration"}
'';
};
accel-profile =
nullable (enum ["adaptive" "flat"])
// {
description = ''
Further reading:
- ${libinput-link "pointer-acceleration" "Pointer acceleration profiles"}
'';
};
scroll-button =
nullable types.int
// {
description = ''
When `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`](https://github.com/torvalds/linux/blob/e42b1a9a2557aa94fee47f078633677198386a52/include/uapi/linux/input-event-codes.h#L355-L363). Most commonly, this will be set to `BTN_LEFT`, `BTN_MIDDLE`, or `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)
Further reading:
- ${libinput-link "scrolling" "On-Button scrolling"}
'';
};
scroll-method =
nullable (types.enum ["no-scroll" "two-finger" "edge" "on-button-down"])
// {
description = ''
When to convert motion events to scrolling events.
The default and supported values vary based on the device type.
Further reading:
- ${libinput-link "scrolling" "Scrolling"}
'';
};
};
pointer-tablet-common = {
enable = optional types.bool true;
left-handed =
optional types.bool false
// {
description = ''
Whether to accomodate left-handed usage for this device.
This varies based on the exact device, but will for example swap left/right mouse buttons.
Further reading:
- ${libinput-link "configuration" "Left-handed Mode"}
'';
};
};
preset-size = dimension: object:
variant {
fixed =
types.int
// {
variant-description = ''
The ${dimension} of the ${object} in logical pixels
'';
};
proportion =
types.float
// {
variant-description = ''
The ${dimension} of the ${object} as a proportion of the screen's ${dimension}
'';
};
};
preset-width = preset-size "width" "column";
preset-height = preset-size "height" "window";
emptyOr = elemType:
mkOptionType {
name = "emptyOr";
description =
if builtins.elem elemType.descriptionClass ["noun" "conjunction"]
then "{} or ${elemType.description}"
else "{} or (${elemType.description})";
descriptionClass = "conjunction";
check = v: v == {} || elemType.check v;
nestedTypes.elemType = elemType;
merge = loc: defs:
if builtins.all (def: def.value == {}) defs
then {}
else elemType.merge loc defs;
inherit (elemType) getSubOptions;
};
default-width = emptyOr preset-width;
link-type = name:
mkOptionType {
name = "shorthand";
description = name;
descriptionClass = "noun";
};
plain-type = description:
mkOptionType
{
name = "plain";
inherit description;
descriptionClass = "noun";
};
newtype = display: inner:
mkOptionType {
name = "newtype";
inherit (display) description descriptionClass;
inherit (inner) check merge getSubOptions;
nestedTypes = {inherit display inner;};
};
# niri seems to have deprecated this way of defining colors; so we won't support it
# color-array = mkOptionType {
# name = "color";
# description = "[red green blue alpha]";
# descriptionClass = "noun";
# check = v: isList v && length v == 4 && all isInt v;
# };
gradient = path:
newtype (plain-type "gradient") (record {
from =
required types.str
// {
description = ''
The starting [`<color>`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) of the gradient.
For more details, see ${link' "${path}.color"}.
'';
};
to =
required types.str
// {
description = ''
The ending [`<color>`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) of the gradient.
For more details, see ${link' "${path}.color"}.
'';
};
angle =
optional types.int 180
// {
description = ''
The angle of the gradient, in degrees, measured clockwise from a gradient that starts at the bottom and ends at the top.
This is the same as the angle parameter in the CSS [`linear-gradient()`](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient) function, except you can only express it in degrees.
'';
};
in' =
nullable (enum [
"srgb"
"srgb-linear"
"oklab"
"oklch shorter hue"
"oklch longer hue"
"oklch increasing hue"
"oklch decreasing hue"
])
// {
description = ''
The colorspace to interpolate the gradient in. This option is named `in'` because `in` is a reserved keyword in Nix.
This is a subset of the [`<color-interpolation-method>`](https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method) values in CSS.
'';
};
relative-to =
optional (enum ["window" "workspace-view"]) "window"
// {
description = ''
The rectangle that this gradient is contained within.
If a gradient is `relative-to` the `"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.
![
four windows arranged in two columns; a big window to the left of three stacked windows.
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.
the three vertical windows look identical, with a yellow and red corner, and the other two corners are slightly different shades of orange.
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.
the top edge of the top stacked window has a noticeable transition from a yellowish orange to completely red.
](assets/relative-to-window.png 'behaviour of relative-to="window"')
If the gradient is instead `relative-to` the `"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
![
four windows arranged in two columns; a big window to the left of three stacked windows.
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.
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.
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.
the bottom edge of the big window is almost entirely yellow, and the top edge of the top stacked window is almost entirely red.
](/assets/relative-to-workspace-view.png 'behaviour of relative-to="workspace-view"')
these beautiful images are sourced from the release notes for ${link-niri-release "0.1.3"}
'';
};
});
decoration = path:
variant {
color =
types.str
// {
variant-description = ''
A solid color to use for the decoration.
This is a CSS [`<color>`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) value, like `"rgb(255 0 0)"`, `"#C0FFEE"`, or `"sandybrown"`.
The specific crate that niri uses to parse this also supports some nonstandard color functions, like `hwba()`, `hsv()`, `hsva()`. See [`csscolorparser`](https://crates.io/crates/csscolorparser) for details.
'';
};
gradient =
(gradient path)
// {
variant-description = ''
A linear gradient to use for the decoration.
This is meant to approximate the CSS [`linear-gradient()`](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient) function, but niri does not fully support all the same parameters. Only an angle in degrees is supported.
'';
};
};
borderish = {
enable-by-default,
default-active-color,
path,
name,
window,
description,
}:
ordered-section [
{
enable =
optional types.bool enable-by-default
// {
description = ''
Whether to enable the ${name}.
'';
};
width =
optional float-or-int 4
// {
description = ''
The width of the ${name} drawn around each ${window}.
'';
};
}
{
active =
optional (newtype (link-type "decoration") (decoration "${path}.active")) {color = default-active-color;}
// {
visible = "shallow";
description = ''
The color of the ${name} for the window that has keyboard focus.
'';
};
inactive =
optional (newtype (link-type "decoration") (decoration "${path}.inactive")) {color = "rgb(80 80 80)";}
// {
visible = "shallow";
description = ''
The color of the ${name} for windows that do not have keyboard focus.
'';
};
}
]
// {
inherit description;
};
border-rule = {
name,
path,
description,
window,
}:
ordered-section [
{
enable =
nullable types.bool
// {
description = ''
Whether to enable the ${name}.
'';
};
width =
nullable float-or-int
// {
description = ''
The width of the ${name} drawn around each ${window}.
'';
};
}
{
active =
nullable (newtype (link-type "decoration") (decoration "${path}.active"))
// {
visible = "shallow";
description = ''
The color of the ${name} for the window that has keyboard focus.
'';
};
inactive =
nullable (newtype (link-type "decoration") (decoration "${path}.inactive"))
// {
visible = "shallow";
description = ''
The color of the ${name} for windows that do not have keyboard focus.
'';
};
}
]
// {
inherit description;
};
regex = newtype (plain-type "regular expression") types.str;
match = newtype (plain-type "match rule") (ordered-record [
{
app-id =
nullable regex
// {
description = ''
A regular expression to match against the app id of the window.
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.
'';
};
title =
nullable regex
// {
description = ''
A regular expression to match against the title of the window.
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.
'';
};
}
{
is-active =
nullable types.bool
// {
description = ''
When non-null, for this field to match a window, the value must match whether the window is active or not.
Every monitor has up to one active window, and `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.
'';
};
is-active-in-column =
nullable types.bool
// {
description = ''
When non-null, for this field to match a window, the value must match whether the window is active in its column or not.
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.
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.
'';
};
is-focused =
nullable types.bool
// {
description = ''
When non-null, for this field to match a window, the value must match whether the window has keyboard focus or not.
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)
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 *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 *window* with keyboard focus.
'';
};
}
{
at-startup =
nullable types.bool
// {
description = ''
When true, this rule will match windows opened within the first 60 seconds of niri starting up. This is useful for setting up initial window positions and sizes.
'';
};
}
]);
alphabetize = sections:
lib.mergeAttrsList (lib.imap0 (i: section: {
${builtins.elemAt lib.strings.lowerChars i} = section;
})
sections);
ordered-record = sections: let
grouped = lib.groupBy (s:
if s ? __module
then "module"
else if s ? __config
then "config"
else "options")
sections;
options' = grouped.options or [];
config' = map (builtins.getAttr "__config") grouped.config or [];
module' = map (builtins.getAttr "__module") grouped.module or [];
normalize = map (flip removeAttrs ["__docs-only"]);
real-sections-flat = pipe options' [
(builtins.filter (s: !(s.__docs-only or false)))
normalize
lib.mergeAttrsList
];
ord-sections = pipe options' [
normalize
alphabetize
];
in
mkOptionType {
inherit
(record (
{config, ...}: {
imports = module';
options = real-sections-flat;
config = lib.mkMerge (map (f:
f config)
config');
}
))
name
description
check
merge
nestedTypes
;
getSubOptions = loc: lib.mapAttrs (_section: opts: (record opts).getSubOptions loc) ord-sections;
};
make-section = flip optional {};
section = flip pipe [record make-section];
ordered-section = flip pipe [ordered-record make-section];
in
ordered-record [
{
switch-events = let
switch-bind = newtype (plain-type "niri switch bind") (record {
action =
required (newtype (plain-type "niri switch action") kdl.types.kdl-leaf)
// {
description = ''
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.
See also ${link' "programs.niri.settings.binds.<name>.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:
```nix
{
programs.niri.settings.switch-events = {
tablet-mode-on.action.spawn = ["gsettings" "set" "org.gnome.desktop.a11y.applications" "screen-keyboard-enabled" "true"];
tablet-mode-off.action.spawn = ["gsettings" "set" "org.gnome.desktop.a11y.applications" "screen-keyboard-enabled" "false"];
};
}
```
'';
};
});
switch-bind' =
nullable (newtype (link-type "switch-bind") switch-bind)
// {
visible = "shallow";
};
in
ordered-section [
{
tablet-mode-on = switch-bind';
tablet-mode-off = switch-bind';
lid-open = switch-bind';
lid-close = switch-bind';
}
{
__docs-only = true;
"<switch-bind>" =
required switch-bind
// {
override-loc = lib.const ["<switch-bind>"];
description = ''
<!--
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.
-->
'';
};
}
];
binds = let
base = record {
allow-when-locked =
optional types.bool false
// {
description = ''
Whether this keybind should be allowed when the screen is locked.
This is only applicable for `spawn` keybinds.
'';
};
cooldown-ms =
nullable types.int
// {
description = ''
The minimum cooldown before a keybind can be triggered again, in milliseconds.
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.
'';
};
repeat =
optional types.bool true
// {
description = ''
Whether this keybind should trigger repeatedly when held down.
'';
};
action =
required (newtype (plain-type "niri action") kdl.types.kdl-leaf)
// {
description = ''
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:
```nix
{
programs.niri.settings.binds = {
"XF86AudioRaiseVolume".action.spawn = ["wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"];
"XF86AudioLowerVolume".action.spawn = ["wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"];
};
}
```
If there is only a single argument, you can pass it directly. It will be implicitly converted to a list in that case.
```nix
{
programs.niri.settings.binds = {
"Mod+D".action.spawn = "fuzzel";
"Mod+1".action.focus-workspace = 1;
};
}
```
For actions taking properties (named arguments), you can pass an attrset.
```nix
{
programs.niri.settings.binds = {
"Mod+Shift+E".action.quit.skip-confirmation = true;
};
}
```
There is also a set of functions available under `config.lib.niri.actions`.
Usage is like so:
```nix
{
programs.niri.settings.binds = with config.lib.niri.actions; {
"XF86AudioRaiseVolume".action = spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+";
"XF86AudioLowerVolume".action = spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-";
"Mod+D".action = spawn "fuzzel";
"Mod+1".action = focus-workspace 1;
"Mod+Shift+E".action = quit;
"Mod+Ctrl+Shift+E".action = quit { skip-confirmation=true; };
"Mod+Plus".action = set-column-width "+10%";
}
}
```
Keep in mind that each one of these attributes (i.e. the nix bindings) are actually identical functions with different node names, and they can take arbitrarily many arguments. The documentation here is based on the *real* acceptable arguments for these actions, but the nix bindings do not enforce this. If you pass the wrong arguments, niri will reject the config file, but evaluation will proceed without problems.
For actions that don't take any arguments, just use the corresponding attribute from `config.lib.niri.actions`. They are listed as `action-name`. For actions that *do* take arguments, they are notated like so: `λ action-name :: <args>`, to clarify that they "should" be used as functions. Hopefully, `<args>` will be clear enough in most cases, but it's worth noting some nontrivial kinds of arguments:
- `size-change`: This is a special argument type used for some actions by niri. It's a string. \
It can take either a fixed size as an integer number of logical pixels (`"480"`, `"1200"`) or a proportion of your screen as a percentage (`"30%"`, `"70%"`) \
Additionally, it can either be an absolute change (setting the new size of the window), or a relative change (adding or subtracting from its size). \
Relative size changes are written with a `+`/`-` prefix, and absolute size changes have no prefix.
- `{ field :: type }`: This means that the action takes a named argument (in kdl, we call it a property). \
To pass such an argument, you should pass an attrset with the key and value. You can pass many properties in one attrset, or you can pass several attrsets with different properties. \
Required fields are marked with `*` before their name, and if no fields are required, you can use the action without any arguments too (see `quit` in the example above). \
If a field is marked with `?`, then omitting it is meaningful. (without `?`, it will have a default value)
- `[type]`: This means that the action takes several arguments as a list. Although you can pass a list directly, it's more common to pass them as separate arguments. \
`spawn ["foo" "bar" "baz"]` is equivalent to `spawn "foo" "bar" "baz"`.
> [!tip]
> You can use partial application to create a spawn command with full support for shell syntax:
> ```nix
> {
> programs.niri.settings.binds = with config.lib.niri.actions; let
> sh = spawn "sh" "-c";
> in {
> "Print".action = sh '''grim -g "$(slurp)" - | wl-copy''';
> };
> }
> ```
${let
show-bind = {
name,
params,
...
}: let
is-stable = builtins.any (a: a.name == name) binds-stable;
is-unstable = builtins.any (a: a.name == name) binds-unstable;
exclusive =
if is-stable && is-unstable
then ""
else if is-stable
then " (only on niri-stable)"
else " (only on niri-unstable)";
type-names = {
LayoutSwitchTarget = ''"next" | "prev"'';
WorkspaceReference = "u8 | string";
SizeChange = "size-change";
bool = "bool";
u8 = "u8";
u16 = "u16";
String = "string";
};
type-or = rust-name: fallback: type-names.${rust-name} or (lib.warn "unhandled type `${rust-name}`" fallback);
base = content: "- `${content}`${exclusive}";
lambda = args: base "λ ${name} :: ${args}";
in
{
empty = base "${name}";
arg = lambda (type-or params.type (
if params.as-str
then "string"
else params.type
));
list = lambda "[${type-or params.type params.type}]";
prop = lambda "{ ${lib.optionalString (!params.use-default) "*"}${params.field}${lib.optionalString params.none-important "?"} :: ${type-names.${params.type} or (lib.warn "unhandled type `${params.type}`" params.type)} }";
unknown = ''
${lambda "unknown"}
The code that generates this documentation does not know how to parse the definition:
```rs
${params.raw-name}(${params.raw})
```
'';
}
.${params.kind}
or (abort "action `${name}` with unhandled kind `${params.kind}` for settings docs");
in
builtins.concatStringsSep "\n" (builtins.concatLists [
(map show-bind (builtins.filter (stable: builtins.all (unstable: stable.name != unstable.name) binds-unstable) binds-stable))
(map show-bind binds-unstable)
])}
'';
};
};
bind = mkOptionType {
inherit (base) name getSubOptions nestedTypes;
description = "niri keybind";
descriptionClass = "noun";
check = v: builtins.isString v || builtins.isAttrs v || base.check v;
merge = loc: defs:
base.merge loc (map (def:
def
// {
value =
if def.value ? action
then def.value
else
lib.warn ''
Deprecated definition of binds used for ${showOption loc}
New properties in niri require a new schema.
Replace binds like `programs.niri.settings.binds."Mod+T".spawn = "alacritty";` with `programs.niri.settings.binds."Mod+T".action.spawn = "alacritty";`.
String actions will also not be supported anymore.
Replace binds like `programs.niri.settings."Mod+Q" = "close-window";` with `programs.niri.settings.binds."Mod+Q".action.close-window = [];`.
This is not an error, and your configuration will still work, but this will not continue to be the case in the future.
Please see the documentation on GitHub for more information: ${link-this-github "docs.md#${anchor' "programs.niri.settings.binds"}"}
''
(
if builtins.isString def.value
then {action.${def.value} = [];}
else {action = def.value;}
);
})
defs);
};
in
attrs bind;
}
{
screenshot-path =
optional (nullOr types.str) "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
// {
description = ''
The path to save screenshots to.
If this is null, then no screenshots will be saved.
If the path starts with a `~`, then it will be expanded to the user's home directory.
The path is then passed to [`stftime(3)`](https://man7.org/linux/man-pages/man3/strftime.3.html) with the current time, and the result is used as the final path.
'';
};
}
{
hotkey-overlay.skip-at-startup =
optional types.bool false
// {
description = ''
Whether to skip the hotkey overlay shown when niri starts.
'';
};
}
{
prefer-no-csd =
optional types.bool false
// {
description = ''
Whether to prefer server-side decorations (SSD) over client-side decorations (CSD).
'';
};
}
{
spawn-at-startup = list (record {
command = list types.str;
});
}
{
workspaces =
attrs (record {
name =
nullable types.str
// {
description = ''
An (optional) name for the workspace. Defaults to the value of the key.
This attribute is intended to be used when you wish to preserve a specific
order for the named workspaces.
'';
};
open-on-output =
nullable types.str
// {
description = ''
The name of the output the workspace should be assigned to.
'';
};
})
// {
description = ''
Declare named workspaces.
Named workspaces are similar to regular, dynamic workspaces, except they can be
referred to by name, and they are persistent, they do not close when there are
no more windows left on them.
Usage is like so:
```nix
{
programs.niri.settings.workspaces."name" = {};
programs.niri.settings.workspaces."01-another-one" = {
open-on-output = "DP-1";
name = "another-one";
};
}
```
Unless a `name` is declared, the workspace will use the attribute key as the name.
Workspaces will be created in a specific order: sorted by key. If you do not care
about the order of named workspaces, you can skip using the `name` attribute, and
use the key instead. If you do care about it, you can use the key to order them,
and a `name` attribute to have a friendlier name.
'';
};
}
{
input = {
keyboard = {
xkb = let
arch-man-xkb = anchor: "[`xkeyboard-config(7)`](https://man.archlinux.org/man/xkeyboard-config.7#${anchor})";
default-env = default: field: ''
If this is set to ${default}, the ${field} will be read from the `XKB_DEFAULT_${lib.toUpper field}` environment variable.
'';
str-fallback = default-env "an empty string";
nullable-fallback = default-env "null";
base = {
layout =
optional types.str ""
// {
description = ''
A comma-separated list of layouts (languages) to include in the keymap.
See ${arch-man-xkb "LAYOUTS"} for a list of available layouts and their variants.
${str-fallback "layout"}
'';
};
model =
optional types.str ""
// {
description = ''
The keyboard model by which to interpret keycodes and LEDs
See ${arch-man-xkb "MODELS"} for a list of available models.
${str-fallback "model"}
'';
};
rules =
optional types.str ""
// {
description = ''
The rules file to use.
The rules file describes how to interpret the values of the model, layout, variant and options fields.
${str-fallback "rules"}
'';
};
variant =
optional types.str ""
// {
description = ''
A comma separated list of variants, one per layout, which may modify or augment the respective layout in various ways.
See ${arch-man-xkb "LAYOUTS"} for a list of available variants for each layout.
${str-fallback "variant"}
'';
};
options =
nullable types.str
// {
description = ''
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.
See ${arch-man-xkb "OPTIONS"} for a list of available options.
If this is set to an empty string, no options will be used.
${nullable-fallback "options"}
'';
};
};
# 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;
in
section base
// {
description = ''
Parameters passed to libxkbcommon, which handles the keyboard in niri.
Further reading:
- [`smithay::wayland::seat::XkbConfig`](https://docs.rs/smithay/latest/smithay/wayland/seat/struct.XkbConfig.html)
'';
};
repeat-delay =
optional types.int 600
// {
description = ''
The delay in milliseconds before a key starts repeating.
'';
};
repeat-rate =