# nirilayout nirilayout is a simple tool to quickly switch your [niri](https://github.com/YaLTeR/niri) output configuration between different layouts. Especially useful for laptop users who move between different setups frequently. > [!NOTE] > This is a fork of [calico32/nirilayout](https://github.com/calico32/nirilayout) > by Caleb Chan. The original repo hasn't been updated in a while, so I'm > keeping it going here. Compared to the original it adds > [watch mode](#automatic-recovery-watch-mode), > [monitor identification](#identify-your-monitors) and > [translations](#localization). ![nirilayout screenshot](screenshot.png) nirilayout works by keeping the `output` blocks for each user-defined layout in separate files in the niri config directory, then symlinking the currently active layout to `nirilayout.kdl` and including that in the main `config.kdl`. A graphical GTK switcher displays the available layouts with a preview of the output arrangement, and allows you to switch between them with the keyboard or mouse. nirilayout-specific comments in the layout files allow you to customize the switcher and the previews. ## Usage ### Install nirilayout **On Arch Linux?** Install it from the AUR: [`nirilayout`](https://aur.archlinux.org/packages/nirilayout) for the latest release, or [`nirilayout-git`](https://aur.archlinux.org/packages/nirilayout-git) to follow `main`. ```sh yay -S nirilayout ``` **On Linux x86_64?** Download the latest precompiled binary from the [releases page](https://github.com/Piero-93/nirilayout/releases). Add it to your path and you're good to go! Otherwise, to build from source, install Go 1.25+, GTK 4.16+ and [GTK4 Layer Shell](https://github.com/wmww/gtk4-layer-shell). Clone this repo, then run `make install` to install nirilayout to $GOBIN. Alternatively, run `make` to build nirilayout to the current directory. ### Configure layouts In `~/.config/niri`, create layouts for your different setups in files named `layout_.kdl`. For example, `layout_vertical.kdl` might look like this: ```kdl //! name "Vertical" //! shortcut "v" //! style font-family="sans-serif" output "Lenovo Group Limited E27q-20 V5HDD696" { //! name "external" mode "2560x1440@74.780" scale 1 position x=0 y=0 } output "BOE 0x0AC1 Unknown" { //! name "laptop" //! style fill=9 mode "2560x1600@120.001" scale 1.5 position x=427 y=1440 } ``` Each layout file should contain the `output` blocks that niri should use in this layout (see [docs](https://niri-wm.github.io/niri/Configuration%3A-Outputs.html)). Then, add special comments throughout the file to configure the switcher. **nirilayout reads the existing KDL as well as any line beginning with `//!` — these are uncommented and parsed as KDL.** As such, follow KDLv1 syntax rules when writing them. #### Global options Global options, outside of any `output` block, apply to the layout as a whole. - **`//! name ""`**: The name of the layout. This is what will be shown in the switcher. If not specified, the name of the file (without the `layout_` prefix and `.kdl` suffix) will be used. - **`//! shortcut ""...`**: The shortcut(s) to use for this layout. You can specify multiple shortcuts by providing several strings (e.g. `//! shortcut "v" "V"`). Shortcuts are case-sensitive. **Shortcuts cannot be prefixes of other shortcuts.** As soon as you finish typing a shortcut, the switcher will immediately select that layout. This means that if you have both "a" and "ab" as shortcuts, typing "a" will immediately select the first layout and you won't be able to type "ab". - **`//! style [prop=value]...`**: Customize the preview style defaults for this layout. You can set any of the following properties: - `fill=`: output fill color (default: auto-assigned based on output name) - `border=`: output border color (default: fill at 1.2× brightness) - `text=`: text color (default: auto-assigned based on fill color) - `border-width=`: border width, pixels (default: 2) - `font-family=""`: output label font family (default: monospace) - `font-size=`: output label font size, pixels (default: 10) - `hide-details=`: whether to hide the output mode and scale in labels (default: false) - `line-spacing=`: line spacing for output labels, pixels (default: 4) #### Per-output configuration These options should be specified inside the `output` block they are configuring. - **`//! name ""`**: Specifies a custom name for this output. If not specified, the name given to niri (`output "..."`) will be used. - **`//! style [prop=value]...`**: Set style properties for this output, which override the layout defaults. See the list of style properties above. - _`//! color ` (deprecated)_: Specifies a custom color for this output. nirilayout will pick a color for each display based on its name, but you can pick a custom color yourself by setting this option to a number between 0 and 17. 0 is gray and 1-17 are preset colors from the Tailwind CSS palette. | Index | Color | | Index | Color | | Index | Color | | ----- | ------ | --- | ----- | ------- | --- | ----- | ------- | | 0 | gray | | 6 | green | | 12 | indigo | | 1 | red | | 7 | emerald | | 13 | violet | | 2 | orange | | 8 | teal | | 14 | purple | | 3 | amber | | 9 | cyan | | 15 | fuchsia | | 4 | yellow | | 10 | sky | | 16 | pink | | 5 | lime | | 11 | blue | | 17 | rose | This option is deprecated in favor of using `//! style fill=`, but is still supported for backwards compatibility. - **`//! mode "WWWxHHH"`**: If you don't want to specify a mode to niri, you'll need to explicitly set a nirilayout-only mode using this option so that the switcher knows how to draw the preview. #### Color values Several color formats are understood: - `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` hex codes - `rgb(R, G, B)` and `rgba(R, G, B, A)` functions, where R/G/B/A are integers between 0 and 255 - A color index between 0 and 17, which corresponds to a preset color in the Tailwind CSS palette (see the table above). Only valid for `fill`/`border` or `//! color`. - A named Tailwind CSS color, like `red500` or `blue200` ### Optional: Configure custom CSS You can further customize the look of the switcher by editing the file `~/.config/niri/nirilayout.css`. Any styles in this file will be loaded as GTK user styles, overriding your GTK theme and the default application styles. You can copy [style.css](style.css) in this repo as a starting point. For example, to make the window translucent: ```css /* In ~/.config/niri/nirilayout.css: */ window { background-color: #00000080; } ``` ### Configure niri Now, run `nirilayout` once to select an initial layout. This creates `~/.config/niri/nirilayout.kdl`, a symlink to the layout you selected. Finally, remove any `output` blocks in `~/.config/niri/config.kdl` and add an include to load `nirilayout.kdl`. ```kdl // In config.kdl: include "nirilayout.kdl" ``` Optionally, add a keybinding to spawn `nirilayout` to make it easier to switch. ```kdl // In config.kdl: binds { // ... Mod+P { spawn "nirilayout"; } // Assuming nirilayout is on $PATH Mod+Shift+I { spawn "nirilayout" "-identify"; } // Show each monitor's name // ... } ``` All done! ## Use nirilayout Run `nirilayout` again (or use your keybinding) to switch between your layouts. In the switcher, you can select a layout with `←`/`→`/`Return` or the mouse, and press `F1` to see which monitor is which. You can also type the name of a layout or its shortcut to select it. Shortcuts and names are case-sensitive. As soon as you finish typing a shortcut, the switcher will immediately select that layout, so make sure your shortcuts are not prefixes of each other. Layouts are presented in lexicographical order by name. If you want to change the order, you can rename the files in the config directory. ### Identify your monitors Layout files name outputs by their connector — `eDP-1`, `HDMI-A-1`, `DP-2` — and with more than one monitor plugged in it is not obvious which name belongs to which screen. Identify mode puts a card in the top-left corner of every monitor for a few seconds — out of the way of the switcher, which is centered — showing exactly the name you would write in a `layout_.kdl`, along with its resolution, refresh rate and scale: ``` eDP-1 1920x1080@60 1× ``` There are three ways to bring it up: - press `F1` (or `Ctrl+I`) while the switcher is open — the switcher stays up and keeps the keyboard, so you can go on picking a layout; - run `nirilayout -identify`, which shows the cards and exits — bind it to a key in niri, see [Configure niri](#configure-niri); - pass `-identify-on-open` to show them every time the switcher opens. This is off by default. The cards disappear on their own after three seconds. Use `-identify-duration` to change that (`-identify-duration 5s`, `-identify-duration 1500ms`); values are kept between 200ms and 30s. Clicking a card, or pressing `Esc`, dismisses them early — in the switcher, the first `Esc` closes the cards and the second closes the switcher. An output that is connected but turned off gets no card, because there is no screen to draw it on. If niri cannot be queried, the cards still appear with the numbers GTK reports; note that those are the logical size, already divided by the scale, rather than the mode. Style the cards with `window.identify`, `label.identify-name` and `label.identify-details` in your custom CSS. See [Configure custom CSS](#optional-configure-custom-css). ### Optional: Skip layouts you cannot use right now By default every layout is selectable, including ones that need a monitor you have not plugged in — applying such a layout leaves that output dark. Two opt-in flags make nirilayout ask niri which outputs are connected and act on the layouts that cannot be applied: ```sh nirilayout -dim-unavailable # keep them visible, but unselectable nirilayout -hide-unavailable # leave them out of the picker entirely ``` A layout is *unavailable* when any output it enables is not connected. Outputs the layout turns `off` never count, so a laptop-only layout stays available with nothing plugged in. With `-dim-unavailable`, unavailable layouts stay in the grid, so you still see they exist, but they are dimmed, labelled with the missing connector, and cannot be selected — by mouse, by `Return`, or by typing their name or shortcut. The arrow keys skip over them. The currently active layout keeps its marker even when it becomes unavailable (you unplugged the monitor after applying it); the selection just starts on the first layout you can actually apply. With `-hide-unavailable`, they are not shown at all, and their shortcuts do nothing. If that leaves nothing to pick, the switcher says so instead of claiming you have no layouts. Passing both flags hides: a layout left out of the picker cannot also be shown dimmed in it. If niri cannot be queried, nirilayout logs the failure and offers every layout rather than dimming or hiding layouts that may well be fine. Style the dimmed entries with `button.unavailable` and `label.missing-outputs` in your custom CSS. See [Configure custom CSS](#optional-configure-custom-css). ## Automatic recovery (watch mode) A layout that turns an output `off` keeps it off even when the situation changes. For example, an "external only" layout with ```kdl //! output "eDP-1" { off } ``` leaves the built-in panel off. If you then unplug the external monitor — or boot with it already unplugged — niri honors that `off` and no output is left drawing, so you get a black screen. nirilayout only ever runs when you launch it by hand, so on its own it cannot react to a cable being pulled. Watch mode fixes this. Run ```sh nirilayout --watch ``` and nirilayout stays in the background instead of showing the GUI. Whenever no output is active, it applies a *safe* layout and niri reloads it, lighting a screen back up. It reacts to niri events immediately and also polls periodically as a safety net. By default the safe layout is auto-picked: among your layouts, nirilayout chooses one whose enabled outputs are all currently connected, preferring the one that lights up the most monitors (so a single laptop panel is used only when nothing better is plugged in). To force a specific layout instead, name it with `--fallback`: ```sh nirilayout --watch --fallback "Laptop Only" ``` To have it always running, spawn it at niri startup: ```kdl // In config.kdl: spawn-at-startup "nirilayout" "--watch" ``` Watch mode never touches the screen while an output is already active, so it is safe to leave running alongside normal use of the GUI switcher. ## Command-line options | Option | Description | | ------------- | -------------------------------------------------------------------------------------------- | | `-c ` | niri config directory (default `~/.config/niri`). | | `-lang `| Interface language code, e.g. `it`. Overrides the system locale. See [Localization](#localization). | | `-lowercase` | Render all of nirilayout's own interface text in lowercase. See [Casing](#casing). | | `-leftalign` | Left-align the text in the search box. See [Search box alignment](#search-box-alignment). | | `-dim-unavailable` | Dim layouts whose outputs are not all connected and make them unselectable. See [Skip layouts you cannot use right now](#optional-skip-layouts-you-cannot-use-right-now). | | `-hide-unavailable` | Leave those layouts out of the picker entirely. Takes precedence over `-dim-unavailable`. See [Skip layouts you cannot use right now](#optional-skip-layouts-you-cannot-use-right-now). | | `-watch` | Run as a background daemon that recovers a black screen instead of showing the GUI. See [Automatic recovery](#automatic-recovery-watch-mode). | | `-fallback ` | In watch mode, the layout to apply when no output is active. See [Automatic recovery](#automatic-recovery-watch-mode). | | `-identify` | Show a card on every connected monitor with its name, resolution and scale, then exit. See [Identify your monitors](#identify-your-monitors). | | `-identify-on-open` | Also show those cards whenever the switcher opens. See [Identify your monitors](#identify-your-monitors). | | `-identify-duration ` | How long the cards stay on screen (default `3s`; accepts `1500ms`, `5s`, …). | Run `nirilayout -h` for the full list. ## Appearance ### Casing By default nirilayout uses standard sentence casing for its own interface text (for example `Esc to quit`). If you prefer everything in lowercase, pass the `-lowercase` flag: ```sh nirilayout -lowercase ``` This only affects nirilayout's own interface strings — the names of your layouts and outputs are always shown exactly as you wrote them. ### Search box alignment The text you type into the search box is centered by default. Pass the `-leftalign` flag to left-align it instead: ```sh nirilayout -leftalign ``` ## Localization nirilayout's interface can be translated. The language is chosen automatically, with the following precedence: 1. The `-lang` flag, when set, wins over everything (e.g. `nirilayout -lang it`). 2. Otherwise the operating system locale is used, read from the `LC_ALL`, `LC_MESSAGES`, and `LANG` environment variables (in that order). A locale like `it_IT.UTF-8` is matched to its base language `it`. 3. Otherwise English is used. If the selected language has no translation, nirilayout falls back to English. Currently available languages: - English (`en`, source language) - Italian (`it`) ### Adding a translation Translations use [gettext](https://www.gnu.org/software/gettext/). The compiled `.mo` catalogs are committed to the repository and embedded into the binary, so **building or running nirilayout never requires any gettext tooling**. You only need the gettext tools (`xgettext`, `msgmerge`, `msgfmt`) to *edit* translations. To add a new language (using French, `fr`, as an example): 1. Refresh the message template and existing translations from the source: ```sh make update-po ``` 2. Create the catalog directory and initialize the `.po` file from the template: ```sh mkdir -p locales/fr/LC_MESSAGES msginit -i locales/nirilayout.pot -o locales/fr/LC_MESSAGES/nirilayout.po -l fr ``` 3. Translate the `msgstr` entries in `locales/fr/LC_MESSAGES/nirilayout.po`. 4. Compile the catalogs (this produces the committed `.mo` files): ```sh make i18n ``` 5. Add the new language to the list above and open a pull request. To update an existing translation, edit its `.po` file, run `make i18n`, and commit both the `.po` and the regenerated `.mo`. # Contributing Contributions are welcome! If you find a bug or have a feature request, please open an issue or a pull request. Translations are especially welcome — see [Adding a translation](#adding-a-translation). # License nirilayout is licensed under the MIT license. See [LICENSE](LICENSE) for more information. Originally written by Caleb Chan ([calico32/nirilayout](https://github.com/calico32/nirilayout)).