Merge branch 'main' of gitea.moonarch.de:nevaforget/moonix
# Conflicts: # DECISIONS.md # modules/services.nix
This commit is contained in:
commit
e205ea6358
10 files changed
+438
-127
No files matched your search
@@ -1,5 +1,40 @@
|
||||
# Decisions
|
||||
|
||||
## 2026-09-08 – nwg-displays replaces wdisplays; kanshi dropped for niri's own outputs
|
||||
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Why**: Monitors could not be set persistently at all. moonarch pairs
|
||||
`wdisplays-persistent` (AUR fork `sfs-pra/wdisplays`, see moonarch `README.md:25` and
|
||||
its `DECISIONS.md:178`) with kanshi: the fork saves straight into the kanshi config.
|
||||
The port took the nixpkgs `wdisplays` instead — upstream, with no save function — and
|
||||
never recorded the difference. The result was a GUI that applies a layout and forgets
|
||||
it, an always-empty `~/.config/kanshi/config`, and a `kanshi.service` whose
|
||||
`ConditionPathExists` therefore skipped it on every boot.
|
||||
- **Tradeoffs**: Packaging the fork would match moonarch exactly and keeps kanshi's
|
||||
per-combination profiles, but it is unmaintained and would be ours to carry.
|
||||
`nwg-displays` is in nixpkgs, actively developed, and supports niri as a first-class
|
||||
target alongside sway and Hyprland; it writes `~/.config/niri/monitor.kdl` and adds the
|
||||
`include` for it. That makes kanshi redundant — niri applies `output` blocks itself, on
|
||||
hotplug and with live reload — so it goes, together with the hardening set it had just
|
||||
been given. What is given up is kanshi's profiles *per attached combination*: niri has
|
||||
one setting per output. Fixed monitors do not care; several docks with different
|
||||
arrangements would, and then kanshi has to come back.
|
||||
The seed is the same mechanism moonarch chose for stasis (its `DECISIONS.md`,
|
||||
2026-04-22): niri has no config hierarchy either, so system-default-plus-user-override
|
||||
cannot be modelled — the home file is seeded once and then belongs to the user.
|
||||
- **How**: `modules/desktop.nix` — `wdisplays` → `nwg-displays`, `kanshi` dropped from
|
||||
`environment.systemPackages`, new `niriUserConfig` in the `let` block holding
|
||||
`include "/etc/niri/config.kdl"`, seeded through the existing
|
||||
`systemd.user.tmpfiles.rules` block with `C` so later edits survive. The stale comment
|
||||
about the undeployed kanshi config is gone from `environment.etc`, the paragraph in
|
||||
`README.md` rewritten. `modules/services.nix` — `systemd.user.services.kanshi` removed
|
||||
and `wlsunset`'s `after` reduced to `graphical-session.target`.
|
||||
Verified: niri 26.04 does load `include` (an include pointing at a deliberately broken
|
||||
file makes `niri validate` fail on that file, so it is parsed, not ignored);
|
||||
`nwg-displays` 0.4.4 logs `Running on niri` and sees both outputs; without
|
||||
`~/.config/niri/` it exits 0 with no window, and it creates `monitor.kdl` but no
|
||||
`config.kdl` — which is what the seed is for.
|
||||
|
||||
## 2026-09-07 – A timer reports the age of the nixpkgs lock
|
||||
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
|
||||
@@ -12,7 +12,7 @@ inside the moonarch config.
|
||||
|
||||
The XDG configuration files (niri, Quickshell, foot, GTK, Qt) come from the
|
||||
`moonarch` repository, which is consumed as a flake input. One set of config
|
||||
files therefore stays valid on both NixOS and Arch.
|
||||
files therefore serves every system that pulls it in.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -272,11 +272,13 @@ NixOS module owns the destination:
|
||||
| `waypaper/config.ini` | wallpaper path, rewritten and seeded into `$HOME` |
|
||||
| fontconfig defaults | `/etc/fonts/conf.d` is owned by the NixOS module |
|
||||
|
||||
`kanshi/config` is not deployed: the file in the config source is empty, and
|
||||
monitor profiles are machine specific — they belong in
|
||||
`~/.config/kanshi/config`. A missing config file is what kanshi treats as an
|
||||
error (`failed to parse config file`), which with `Restart=on-failure` would
|
||||
spin, so the service starts only once that file exists.
|
||||
`kanshi/config` is not deployed and kanshi is not installed: niri configures its
|
||||
outputs itself, and `nwg-displays` writes the layout it is given to
|
||||
`~/.config/niri/monitor.kdl`. niri prefers a home config over the deployed one
|
||||
and does not merge them, so `~/.config/niri/config.kdl` is seeded with an
|
||||
`include` of `/etc/niri/config.kdl` — the home config then adds the monitor
|
||||
layout instead of replacing everything. nwg-displays also refuses to start
|
||||
without that file.
|
||||
|
||||
Programs that read only from `$HOME` and have no system-wide fallback get a
|
||||
copy seeded through `systemd.user.tmpfiles` with the `C` directive, which never
|
||||
|
||||
Generated
+10
-10
@@ -24,11 +24,11 @@
|
||||
"moonarch": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1787827834,
|
||||
"narHash": "sha256-3yGZz3/JAtCmAQrh8jwTIAd6xCbE3O7e2Iar6AVnbLk=",
|
||||
"lastModified": 1788961983,
|
||||
"narHash": "sha256-ndUyq3X3E5vVdUflbCxdd2c+CBlXcNgn0rc5qiovCUM=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "a1e06bf380b8c79a646930881d76cbe7137b4b77",
|
||||
"revCount": 169,
|
||||
"rev": "ba3745603cd01508669a70e56527e3d471a0d5f6",
|
||||
"revCount": 179,
|
||||
"type": "git",
|
||||
"url": "https://gitea.moonarch.de/nevaforget/moonarch.git"
|
||||
},
|
||||
@@ -42,11 +42,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1787144466,
|
||||
"narHash": "sha256-HHfv2/HkNSKbbSyU9iD/g8lbP6r4tl33sSw1W4rXCk0=",
|
||||
"lastModified": 1788335262,
|
||||
"narHash": "sha256-3jBEq8avfzlrsY4UpW4d5TOmm7ocseZfnitEJhqauyc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixos-hardware",
|
||||
"rev": "0471accf8d0a8210b31d947497d179ecc99e0021",
|
||||
"rev": "44d95795ee2d475b3d687325e26dcf4ca9104557",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -70,11 +70,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1787736819,
|
||||
"narHash": "sha256-cV5xEJJK3BvhU8rEd4mC9UsmDi5qscv/kzGPhBRC5WA=",
|
||||
"lastModified": 1788316716,
|
||||
"narHash": "sha256-bc7rSpXIdn9QWGNqfWcPZWOhEVF8NoeAZkWq0XWnf/k=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9fbb54b33e91ee4ca368e35a78e0613c720600b3",
|
||||
"rev": "3ed67ec0a4d3c7ab4ae1f04f8ee8df07bfa506a2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
};
|
||||
|
||||
# Project sources. These repos are not flakes themselves, they are
|
||||
# consumed as plain source trees pinned to a release tag.
|
||||
# consumed as plain source trees following their default branch.
|
||||
moonarch = {
|
||||
url = "git+https://gitea.moonarch.de/nevaforget/moonarch.git";
|
||||
flake = false;
|
||||
|
||||
+22
-27
@@ -5,14 +5,22 @@
|
||||
|
||||
{
|
||||
# Read off the machine with `ls -l /dev/disk/by-id/`: the 512G XPG is the
|
||||
# system disk, the 1T Crucial carries /home. disko-install still overrides
|
||||
# both with `--disk main <path>` and `--disk home <path>`, so check them
|
||||
# against lsblk before installing — by-id names follow the drive, not the
|
||||
# slot, and a replaced disk changes them.
|
||||
# system disk and carries /home as well. disko-install still overrides it with
|
||||
# `--disk main <path>`, so check that against lsblk before installing — by-id
|
||||
# names follow the drive, not the slot, and a replaced disk changes them.
|
||||
#
|
||||
# The 1T Crucial P1 carried /home until 09.09.2026 and now takes the storage
|
||||
# role instead, which is the only one it can still be trusted with. Its
|
||||
# controller reaches 90 °C under pure read load while the rest of the drive
|
||||
# stays at 45, throttles from 1.5 GB/s down to under a megabyte, and it fell
|
||||
# off the PCIe bus four times in four days (`controller is down,
|
||||
# CSTS=0xffffffff`), forcing /home read-only. SMART is clean and P3CR013 is the
|
||||
# final firmware, so there is nothing to repair — but as storage its failures
|
||||
# cost a download, not a workday, and it can no longer delay a boot.
|
||||
moonarch.disk = {
|
||||
enable = true;
|
||||
device = "/dev/disk/by-id/nvme-XPG_GAMMIX_S11_Pro_2K322LAES7JG";
|
||||
home = {
|
||||
storage = {
|
||||
enable = true;
|
||||
device = "/dev/disk/by-id/nvme-CT1000P1SSD8_2030E2BAC109";
|
||||
};
|
||||
@@ -67,28 +75,15 @@
|
||||
user = config.moonarch.user;
|
||||
};
|
||||
|
||||
# Snapshots of root and home. snap-pac has no counterpart here: NixOS keeps
|
||||
# its own generations, so pre/post package snapshots are redundant. /home is
|
||||
# its own btrfs on the second disk, which snapper does not care about — a
|
||||
# subvolume is a subvolume.
|
||||
services.snapper = {
|
||||
configs = {
|
||||
root = {
|
||||
SUBVOLUME = "/";
|
||||
ALLOW_USERS = [ config.moonarch.user ];
|
||||
TIMELINE_CREATE = true;
|
||||
TIMELINE_CLEANUP = true;
|
||||
};
|
||||
home = {
|
||||
SUBVOLUME = "/home";
|
||||
ALLOW_USERS = [ config.moonarch.user ];
|
||||
TIMELINE_CREATE = true;
|
||||
TIMELINE_CLEANUP = true;
|
||||
};
|
||||
};
|
||||
snapshotInterval = "hourly";
|
||||
cleanupInterval = "1d";
|
||||
};
|
||||
# No snapper here. It was configured for / and /home and never took a single
|
||||
# snapshot: the module writes the configs and the timers, but nobody creates
|
||||
# the .snapshots subvolumes they need, so snapper-timeline failed every hour
|
||||
# with errno:2 for months without anyone noticing. Rather than paper over that,
|
||||
# note what it would and would not have bought: rollbacks of / are already
|
||||
# covered by NixOS generations, and snapshots of /home share a disk with /home,
|
||||
# so they answer "I deleted the wrong file" but not "the disk is gone" — which
|
||||
# is the failure that actually happened here on 09.09.2026. The second question
|
||||
# needs a copy off this machine and is still unanswered.
|
||||
|
||||
# No nixos-hardware profile applies to a self-built desktop, so the firmware
|
||||
# this machine needs is enabled here instead.
|
||||
|
||||
+93
-33
@@ -8,7 +8,7 @@ let
|
||||
etc = "${moonarchSrc}/defaults/etc";
|
||||
|
||||
# waypaper calls `swww`/`swww-daemon`, but the maintained successor ships as
|
||||
# `awww`. Same compatibility symlinks the Arch package creates.
|
||||
# `awww`. The symlinks keep the shared config working unchanged.
|
||||
swww-compat = pkgs.runCommand "swww-compat" { } ''
|
||||
mkdir -p "$out/bin"
|
||||
ln -s ${pkgs.awww}/bin/awww "$out/bin/swww"
|
||||
@@ -28,14 +28,28 @@ let
|
||||
# Same treatment for swaylock's wallpaper path. swaylock searches
|
||||
# ~/.swaylock/config, $XDG_CONFIG_HOME/swaylock/config and
|
||||
# SYSCONFDIR/swaylock/config — and its sysconfdir is inside the store here,
|
||||
# so the home copy seeded below is the only one it ever reads. On Arch the
|
||||
# same file ships to /etc/swaylock/config, which is why it lives in
|
||||
# so the home copy seeded below is the only one it ever reads. The file is a
|
||||
# system config rather than an XDG one, which is why moonarch keeps it under
|
||||
# defaults/etc/ rather than defaults/xdg/.
|
||||
swaylockConfig = pkgs.runCommand "swaylock-config" { } ''
|
||||
substitute ${etc}/swaylock/config "$out" \
|
||||
--replace-fail /usr/share/moonarch/wallpaper-blur.jpg ${wallpaperBlur}
|
||||
'';
|
||||
|
||||
# niri prefers ~/.config/niri/config.kdl over /etc/niri/config.kdl and does not
|
||||
# merge the two, so a home config would otherwise replace the whole deployed
|
||||
# one. This seed pulls the system config back in through `include`, which niri
|
||||
# 26.04 really does load (verified by pointing an include at a broken file and
|
||||
# watching `niri validate` fail on it).
|
||||
#
|
||||
# It exists for nwg-displays, which writes the output layout to
|
||||
# ~/.config/niri/monitor.kdl and adds an include for it here. Without a
|
||||
# config.kdl in the home directory nwg-displays refuses to start at all
|
||||
# ("Couldn't find niri config directory", exit 0, no window).
|
||||
niriUserConfig = pkgs.writeText "niri-config.kdl" ''
|
||||
include "/etc/niri/config.kdl"
|
||||
'';
|
||||
|
||||
# PipeWire searches $PIPEWIRE_CONFIG_DIR, then $XDG_CONFIG_HOME/pipewire, then
|
||||
# its compiled-in /etc/pipewire — never /etc/xdg (verified in PipeWire's
|
||||
# src/pipewire/conf.c). The shared file therefore cannot be deployed the way
|
||||
@@ -43,7 +57,7 @@ let
|
||||
# which is what its own option documentation shows for a conf.d file.
|
||||
#
|
||||
# The file names the plugin as `librnnoise_ladspa`, resolved through
|
||||
# LADSPA_PATH — /usr/lib/ladspa on Arch, nothing here. Substituted to the
|
||||
# LADSPA_PATH, which points at nothing here. Substituted to the
|
||||
# store path, like the wallpaper paths above. LADSPA has no counterpart to
|
||||
# services.pipewire.extraLv2Packages, so the path belongs in the file.
|
||||
denoisingConfig = pkgs.runCommand "moonarch-pipewire-denoising" { } ''
|
||||
@@ -54,12 +68,22 @@ let
|
||||
"plugin = ${pkgs.rnnoise-plugin}/lib/ladspa/librnnoise_ladspa.so"
|
||||
'';
|
||||
|
||||
# The theme deployed Kvantum/kvantum.kvconfig selects. Of the four places
|
||||
# Kvantum searches (style/Kvantum.cpp), three are under $HOME and the fourth
|
||||
# is a DATADIR compiled into the style plugin — which under Nix is the
|
||||
# plugin's own store path. A separate theme package is therefore unreachable
|
||||
# system-wide, and the directory is seeded into the home directory below.
|
||||
kvantumTheme = pkgs.catppuccin-kvantum.override {
|
||||
accent = "lavender";
|
||||
variant = "mocha";
|
||||
};
|
||||
|
||||
# zsh/newuser runs its setup wizard on every interactive login for as long as
|
||||
# the user has none of .zshenv, .zprofile, .zshrc or .zlogin — the whole
|
||||
# configuration living in /etc/zshrc does not count. Seeded below to stop
|
||||
# that, and deliberately without configuration: on Arch this file carries
|
||||
# `source /etc/zsh/zshrc.moonarch`, which here would source the shared zshrc
|
||||
# a second time, on top of the one /etc/zshrc already did.
|
||||
# that, and deliberately without configuration: moonarch's own copy of this
|
||||
# file sources the shared zshrc, which here would run it a second time, on
|
||||
# top of the one /etc/zshrc already did.
|
||||
userZshrc = pkgs.writeText "zshrc" ''
|
||||
# The moonarch zsh configuration is system-wide, in /etc/zshrc.
|
||||
#
|
||||
@@ -77,17 +101,12 @@ in
|
||||
environment.etc = {
|
||||
# niri reads /etc/niri, not /etc/xdg/niri. Both are provided: the former
|
||||
# is what niri actually loads, the latter keeps the layout consistent with
|
||||
# the Arch package and with any user config that includes it.
|
||||
# moonarch's tree and with any user config that includes it.
|
||||
"niri/config.kdl".source = "${xdg}/niri/config.kdl";
|
||||
"xdg/niri/config.kdl".source = "${xdg}/niri/config.kdl";
|
||||
"xdg/quickshell/moonarch".source = "${xdg}/quickshell/moonarch";
|
||||
"xdg/foot/foot.ini".source = "${xdg}/foot/foot.ini";
|
||||
"xdg/fastfetch/config.jsonc".source = "${xdg}/fastfetch/config.jsonc";
|
||||
# defaults/xdg/kanshi/config is an empty placeholder — on Arch too. Not
|
||||
# deployed: monitor profiles are machine specific and belong in
|
||||
# ~/.config/kanshi/config. An empty file parses fine; a missing one is what
|
||||
# kanshi reports as "failed to parse config file", which is why the unit in
|
||||
# modules/services.nix waits for the home config to exist.
|
||||
"xdg/Kvantum/kvantum.kvconfig".source = "${xdg}/Kvantum/kvantum.kvconfig";
|
||||
"xdg/qt6ct/qt6ct.conf".source = "${xdg}/qt6ct/qt6ct.conf";
|
||||
"xdg/gtk-3.0/settings.ini".source = "${xdg}/gtk-3.0/settings.ini";
|
||||
@@ -95,15 +114,14 @@ in
|
||||
# No xdg/pipewire/ entry: PipeWire does not read /etc/xdg, so the file was
|
||||
# deployed and inert. It goes through services.pipewire.configPackages
|
||||
# below instead.
|
||||
# Kept for layout parity with the Arch package; waypaper itself only reads
|
||||
# Kept for layout parity with moonarch's tree; waypaper itself only reads
|
||||
# the copy in the home directory, seeded below.
|
||||
"xdg/waypaper/config.ini".source = waypaperConfig;
|
||||
|
||||
# stasis reads /etc/stasis/ as its system-wide fallback, never /etc/xdg/.
|
||||
"stasis/stasis.rune".source = "${xdg}/stasis/stasis.rune";
|
||||
# No stasis entry: /etc/stasis/ is only its fallback for a missing user
|
||||
# config, and the seed below means there never is one.
|
||||
|
||||
# Configs outside /etc/xdg/ in the Arch package as well.
|
||||
"mpv/mpv.conf".source = "${etc}/mpv/mpv.conf";
|
||||
# Configs that live outside defaults/xdg/ in moonarch as well.
|
||||
"xdg-desktop-portal/niri-portals.conf".source =
|
||||
"${etc}/xdg-desktop-portal/niri-portals.conf";
|
||||
};
|
||||
@@ -140,13 +158,12 @@ in
|
||||
programs.niri.enable = true;
|
||||
|
||||
# niri searches $XDG_CONFIG_HOME/niri/config.kdl, then /etc/niri/config.kdl —
|
||||
# not /etc/xdg. On Arch a one-line ~/.config/niri/config.kdl bridges there
|
||||
# via `include`; here the system path is used directly, so nothing has to be
|
||||
# placed in the home directory.
|
||||
# not /etc/xdg. The system path is used directly, so nothing has to be placed
|
||||
# in the home directory to bridge there.
|
||||
|
||||
# config.kdl spawns the polkit agent by its FHS path
|
||||
# (/usr/lib/polkit-gnome/...). Provide that path rather than diverging the
|
||||
# config, which is shared with Arch.
|
||||
# config, which moonarch owns.
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /usr/lib 0755 root root -"
|
||||
"L+ /usr/lib/polkit-gnome - - - - ${pkgs.polkit_gnome}/libexec"
|
||||
@@ -155,7 +172,7 @@ in
|
||||
# --- Shell ---
|
||||
programs.zsh.enable = true;
|
||||
|
||||
# Same file the Arch package installs as /etc/zsh/zshrc.moonarch. zsh reads
|
||||
# moonarch's shared zshrc, which it installs as /etc/zsh/zshrc.moonarch. zsh reads
|
||||
# /etc/zshrc before ~/.zshrc, so a user file still wins over it.
|
||||
programs.zsh.interactiveShellInit = ''
|
||||
source ${moonarchSrc}/defaults/shell/zshrc
|
||||
@@ -167,10 +184,9 @@ in
|
||||
# The zshrc owns the prompt, so nothing else has to run here.
|
||||
programs.zsh.promptInit = "";
|
||||
|
||||
# The zshrc sources both plugins from /usr/share/zsh/plugins, the path the
|
||||
# Arch packages use; the lookups are guarded and silently find nothing here.
|
||||
# These modules source the same plugins from the store instead. The strategy
|
||||
# is the one the zshrc sets on Arch.
|
||||
# The zshrc sources both plugins from /usr/share/zsh/plugins; the lookups are
|
||||
# guarded and silently find nothing here. These modules source the same
|
||||
# plugins from the store instead, with the strategy the zshrc sets.
|
||||
programs.zsh.syntaxHighlighting.enable = true;
|
||||
programs.zsh.autosuggestions = {
|
||||
enable = true;
|
||||
@@ -210,6 +226,14 @@ in
|
||||
}];
|
||||
};
|
||||
|
||||
# Qt applications are wrapped, so each one looks for plugins in its own
|
||||
# closure and never finds a style installed alongside it — which is why VLC
|
||||
# kept coming up in the stock Qt style. Enabling this puts the profile's Qt5
|
||||
# and Qt6 plugin directories on QT_PLUGIN_PATH, which the wrappers append to.
|
||||
# platformTheme and style stay null: it would add packages that are already
|
||||
# listed below, and niri's environment block sets both variables already.
|
||||
qt.enable = true;
|
||||
|
||||
# --- Portals ---
|
||||
xdg.portal = {
|
||||
enable = true;
|
||||
@@ -268,7 +292,7 @@ in
|
||||
moonarchPkgs.sweet-cursors
|
||||
moonarchPkgs.moonarch-scripts
|
||||
|
||||
# Browser. Arch runs waterfox-bin, which nixpkgs does not carry; see
|
||||
# Browser. moonarch runs waterfox-bin, which nixpkgs does not carry; see
|
||||
# DECISIONS.md for why this is not packaged here.
|
||||
librewolf
|
||||
|
||||
@@ -277,17 +301,27 @@ in
|
||||
|
||||
# Compositor extras
|
||||
xwayland-satellite
|
||||
kanshi
|
||||
|
||||
# Bar, notifications, clipboard.
|
||||
# Neither waybar nor swaync are installed: the Quickshell bar owns both
|
||||
# roles. The "kept as a reserve" notes in the Arch config date from the
|
||||
# roles. The "kept as a reserve" notes in moonarch's config date from the
|
||||
# migration and do not apply to a fresh setup.
|
||||
quickshell
|
||||
libnotify
|
||||
cliphist
|
||||
wl-clipboard
|
||||
|
||||
# The launcher's start prefix, and why the launcher needs one: Quickshell
|
||||
# spawns applications as its own children, and a child inherits its
|
||||
# parent's cgroup, so everything started from the launcher lands inside
|
||||
# quickshell.service — which systemd kills whole on every restart of the
|
||||
# unit (KillMode=control-group is the default). A crash of the bar took the
|
||||
# open browser with it, and Librewolf counted each one as a crash until it
|
||||
# stopped restoring the session at all. app2unit wraps systemd-run to put
|
||||
# each application in its own transient app-*.scope, the same shape niri
|
||||
# gives the windows it spawns itself.
|
||||
app2unit
|
||||
|
||||
# Display and wallpaper
|
||||
wlsunset
|
||||
nwg-look
|
||||
@@ -349,10 +383,9 @@ in
|
||||
git
|
||||
|
||||
# System
|
||||
fwupd
|
||||
upower
|
||||
polkit_gnome
|
||||
wdisplays
|
||||
nwg-displays
|
||||
wl-color-picker
|
||||
|
||||
stasis
|
||||
@@ -372,15 +405,42 @@ in
|
||||
# Set by PAM at session start, so it takes a re-login, not just a rebuild.
|
||||
environment.sessionVariables.NIXPKGS_ALLOW_UNFREE = "1";
|
||||
|
||||
# niri sets both in its own environment block, but that only reaches what niri
|
||||
# itself spawns. Anything under the systemd user manager — quickshell, and so
|
||||
# everything started from its launcher — never sees them, and Qt applications
|
||||
# come up unstyled. Set here they travel through /etc/profile into
|
||||
# niri-session's import-environment, and from there into the user manager.
|
||||
environment.sessionVariables.QT_QPA_PLATFORMTHEME = "qt6ct";
|
||||
environment.sessionVariables.QT_STYLE_OVERRIDE = "kvantum";
|
||||
|
||||
# Same gap, cursors: niri's cursor block reaches xwayland-satellite, so X11
|
||||
# clients have a themed pointer available, but everything under the user
|
||||
# manager arrives with XCURSOR_PATH alone and has no theme name to look up.
|
||||
# The name matches modules/greetd.nix, so greeter and session agree.
|
||||
environment.sessionVariables.XCURSOR_THEME = "Sweet-cursors";
|
||||
environment.sessionVariables.XCURSOR_SIZE = "24";
|
||||
|
||||
# Seed configs for programs that read only from the home directory and have
|
||||
# no system-wide fallback. 'C' copies once and never overwrites, so user
|
||||
# edits survive. This is what post-install.sh does imperatively on Arch.
|
||||
# edits survive. This is what moonarch's post-install.sh does imperatively.
|
||||
systemd.user.tmpfiles.rules = [
|
||||
"C %h/.config/Kvantum/catppuccin-mocha-lavender 0755 - - - ${kvantumTheme}/share/Kvantum/catppuccin-mocha-lavender"
|
||||
"C %h/.config/waypaper/config.ini 0644 - - - ${waypaperConfig}"
|
||||
# stasis prefers the user config over /etc and generates its own if none
|
||||
# exists, so the moonarch config has to be in place before it starts.
|
||||
"C %h/.config/stasis/stasis.rune 0644 - - - ${xdg}/stasis/stasis.rune"
|
||||
"C %h/.config/swaylock/config 0644 - - - ${swaylockConfig}"
|
||||
# nwg-displays needs this file to exist before it will start, and niri would
|
||||
# otherwise ignore the monitor.kdl it writes next to it.
|
||||
"C %h/.config/niri/config.kdl 0644 - - - ${niriUserConfig}"
|
||||
"C %h/.zshrc 0644 - - - ${userZshrc}"
|
||||
# Steam gets XCURSOR_THEME from the block above — verified in its own
|
||||
# environment — and still drew the core pointer, so parts of it resolve the
|
||||
# theme literally named 'default' rather than the variable. Xcursor looks
|
||||
# that name up along XCURSOR_PATH like any other, and ~/.icons is first,
|
||||
# so pointing it at the theme covers those lookups too. 'L+' replaces the
|
||||
# link on every activation, unlike the 'C' rules above, which keeps it
|
||||
# following the theme instead of freezing at today's one.
|
||||
"L+ %h/.icons/default - - - - /run/current-system/sw/share/icons/Sweet-cursors"
|
||||
];
|
||||
}
|
||||
@@ -30,6 +30,30 @@ let
|
||||
"/home" = { mountpoint = "/home"; inherit mountOptions; };
|
||||
};
|
||||
|
||||
# A disk holding nothing anyone waits for is allowed to be absent. These
|
||||
# options make that a non-event: the mount happens on first access rather than
|
||||
# at boot, systemd stops waiting for the device after five seconds, and the
|
||||
# filesystem is unmounted again once nothing has touched it for ten minutes,
|
||||
# which leaves the disk idle and cool most of the time.
|
||||
scratchMountOptions = mountOptions ++ [
|
||||
"noauto"
|
||||
"nofail"
|
||||
"x-systemd.automount"
|
||||
"x-systemd.device-timeout=5"
|
||||
"x-systemd.mount-timeout=10"
|
||||
"x-systemd.idle-timeout=600"
|
||||
# Without this the disk is reachable only by typing its path: file managers
|
||||
# list removable media and gvfs entries, not plain fstab lines. Since the
|
||||
# entry is noauto, it shows up as a drive that is not mounted yet, which is
|
||||
# what it usually is.
|
||||
"x-gvfs-show"
|
||||
"x-gvfs-name=Storage"
|
||||
];
|
||||
|
||||
storageSubvolumes = {
|
||||
"/storage" = { mountpoint = "/storage"; mountOptions = scratchMountOptions; };
|
||||
};
|
||||
|
||||
btrfs = subvolumes: {
|
||||
type = "btrfs";
|
||||
# Overwrite an existing signature instead of asking. The device is erased
|
||||
@@ -56,6 +80,13 @@ let
|
||||
inherit content;
|
||||
};
|
||||
|
||||
# Same container, opened after stage 1 instead of inside the initrd. A disk
|
||||
# that stage 1 waits for can stop the machine from booting when it is missing
|
||||
# or has died, which is exactly what must not happen for expendable storage;
|
||||
# initrdUnlock = false keeps it out of there. Nothing then opens it on its own,
|
||||
# so the caller has to supply a crypttab entry — see storageCrypttab below.
|
||||
luksAfterInitrd = name: content: (luks name content) // { initrdUnlock = false; };
|
||||
|
||||
# Wraps a filesystem in LUKS unless the host opted out of encryption.
|
||||
maybeEncrypted = name: subvolumes:
|
||||
if cfg.encrypt then luks name (btrfs subvolumes) else btrfs subvolumes;
|
||||
@@ -100,6 +131,45 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
storage = {
|
||||
enable = lib.mkEnableOption "a disk for data whose loss costs nothing";
|
||||
|
||||
device = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "/dev/disk/by-id/nvme-eui.0f1e2d3c4b5a6978";
|
||||
description = ''
|
||||
Whole disk for expendable data, mounted at /storage — a game library,
|
||||
downloads, images. Nothing that would be missed and nothing anyone
|
||||
waits for, because this disk is allowed to be slow, absent or dead.
|
||||
|
||||
Unlike the other two it is never unlocked in the initrd, so a failure
|
||||
cannot hold up the boot. It is mounted on first access instead and
|
||||
unmounted again when idle. That is what makes it usable for a drive
|
||||
that is not entirely trustworthy.
|
||||
|
||||
disko-install addresses it as `--disk storage <path>`.
|
||||
'';
|
||||
};
|
||||
|
||||
keyFile = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/var/keys/storage.key";
|
||||
description = ''
|
||||
Key file that unlocks the container without a prompt. It has to live on
|
||||
an already-decrypted filesystem — the default sits on the root disk, so
|
||||
it is covered by that disk's encryption and unreadable without the boot
|
||||
passphrase.
|
||||
|
||||
Create it once and add it as a second key slot; the existing passphrase
|
||||
keeps working:
|
||||
|
||||
dd bs=512 count=4 if=/dev/random of=<keyFile> iflag=fullblock
|
||||
chmod 400 <keyFile>
|
||||
cryptsetup luksAddKey <partition> <keyFile>
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
encrypt = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
@@ -178,5 +248,36 @@ in
|
||||
};
|
||||
};
|
||||
})
|
||||
|
||||
# Expendable storage, same shape as the disk above with one difference that
|
||||
# changes everything about it: its container never enters the initrd, so a
|
||||
# drive that is slow, missing or dead cannot stop the machine from booting.
|
||||
(lib.mkIf cfg.storage.enable {
|
||||
disko.devices.disk.storage = {
|
||||
type = "disk";
|
||||
device = cfg.storage.device;
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions.storage = {
|
||||
size = "100%";
|
||||
content =
|
||||
if cfg.encrypt
|
||||
then luksAfterInitrd "storage" (btrfs storageSubvolumes)
|
||||
else btrfs storageSubvolumes;
|
||||
};
|
||||
};
|
||||
};
|
||||
})
|
||||
|
||||
# Keeping it out of the initrd means nothing opens it any more, so this is
|
||||
# what does. The partition is addressed by its partlabel, which disko derives
|
||||
# from the disk and partition names above — no UUID has to be pasted in here,
|
||||
# and a replaced disk needs no edit. noauto leaves the opening to the mount
|
||||
# in storageSubvolumes, nofail keeps a failure from becoming a boot problem.
|
||||
(lib.mkIf (cfg.storage.enable && cfg.encrypt) {
|
||||
environment.etc.crypttab.text = ''
|
||||
storage /dev/disk/by-partlabel/disk-storage-storage ${cfg.storage.keyFile} noauto,nofail
|
||||
'';
|
||||
})
|
||||
]);
|
||||
}
|
||||
+12
-1
@@ -1,7 +1,7 @@
|
||||
# ABOUTME: Sets up greetd with regreet as the login greeter.
|
||||
# ABOUTME: Counterpart to defaults/etc/greetd/ in the moonarch repo.
|
||||
|
||||
{ pkgs, lib, moonarchSrc, moonarchPkgs, moonarchThemes, ... }:
|
||||
{ config, pkgs, lib, moonarchSrc, moonarchPkgs, moonarchThemes, ... }:
|
||||
|
||||
let
|
||||
wallpaper = "${moonarchSrc}/defaults/backgrounds/wallpaper.jpg";
|
||||
@@ -11,6 +11,10 @@ in
|
||||
{
|
||||
environment.etc."greetd/niri-greeter.kdl".source = greeterNiriConfig;
|
||||
|
||||
# niri-greeter.kdl spawns `regreet` by name, and the module only installs it
|
||||
# through the default_session command that is overridden below.
|
||||
environment.systemPackages = [ config.services.displayManager.regreet.package ];
|
||||
|
||||
# /etc/greetd/regreet.toml and regreet.css are written by the module, so
|
||||
# defaults/etc/greetd/regreet.toml is not deployed verbatim: its wallpaper
|
||||
# path points at /usr/share/moonarch/, which does not exist here. Every other
|
||||
@@ -63,6 +67,13 @@ in
|
||||
|
||||
services.greetd = {
|
||||
enable = true;
|
||||
|
||||
# The option defaults to false as soon as a host sets initial_session, on
|
||||
# the grounds that a restart would autologin again. greetd(5) says it does
|
||||
# not: the initial session runs only on the first launch since boot, marked
|
||||
# by a runfile under /run. Left at the default, logging out ends greetd and
|
||||
# leaves a dead VT with no way back in.
|
||||
restart = true;
|
||||
settings.default_session = {
|
||||
# regreet needs a Wayland compositor. niri instead of the module's cage
|
||||
# default: niri-greeter.kdl carries the German keyboard layout, numlock
|
||||
|
||||
+154
-47
@@ -1,7 +1,7 @@
|
||||
# ABOUTME: systemd system and user services for the desktop session.
|
||||
# ABOUTME: Mostly rebuilt from moonarch/defaults/etc/systemd/, whose units use FHS paths.
|
||||
|
||||
{ pkgs, lib, moonarchPkgs, ... }:
|
||||
{ config, pkgs, lib, moonarchPkgs, ... }:
|
||||
|
||||
let
|
||||
# Days after which the nixpkgs input counts as stale. nixpkgs carries no
|
||||
@@ -73,6 +73,88 @@ let
|
||||
read -r -p '[enter to close] ' _"
|
||||
'';
|
||||
};
|
||||
|
||||
# Shared by the two root oneshots below. Both read one state file and write a
|
||||
# single sysfs attribute, so everything else is taken away. Scored with
|
||||
# `systemd-analyze security`: 7.5 EXPOSED before, 0.9 SAFE after.
|
||||
#
|
||||
# Deliberately absent:
|
||||
# ProtectKernelTunables - remounts /sys read-only, which is exactly the
|
||||
# write both units exist for. ReadWritePaths is no fix, /sys/class/… is a
|
||||
# symlink into /sys/devices/….
|
||||
# PrivateUsers - capabilities inside a user namespace are namespace-local,
|
||||
# while the kernel's sysfs store handlers test against the initial one.
|
||||
#
|
||||
# ProtectSystem=strict is safe here: it leaves /dev, /proc and /sys writable,
|
||||
# and the state file is only ever read.
|
||||
sysfsWriterHardening = {
|
||||
CapabilityBoundingSet = [ "" ];
|
||||
AmbientCapabilities = [ "" ];
|
||||
NoNewPrivileges = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
PrivateNetwork = true;
|
||||
IPAddressDeny = "any";
|
||||
RestrictAddressFamilies = [ "AF_UNIX" ];
|
||||
ProtectClock = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
KeyringMode = "private";
|
||||
UMask = "0077";
|
||||
};
|
||||
|
||||
# Shared by the Wayland session clients below: socket to the compositor,
|
||||
# nothing else. 9.4 UNSAFE before, 0.9 SAFE after, each one started under the
|
||||
# full set with `systemd-run --user` first.
|
||||
#
|
||||
# Deliberately absent:
|
||||
# ProtectHome - hides /home, /root *and /run/user*, where the Wayland
|
||||
# socket lives. Read-only fails too: connect() needs write on the inode.
|
||||
# PrivateUsers - maps the session uid to nobody, the socket is owned by the
|
||||
# real one.
|
||||
waylandClientHardening = {
|
||||
CapabilityBoundingSet = [ "" ];
|
||||
AmbientCapabilities = [ "" ];
|
||||
NoNewPrivileges = true;
|
||||
ProtectSystem = "strict";
|
||||
ReadWritePaths = [ "%t" ];
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
PrivateNetwork = true;
|
||||
IPAddressDeny = "any";
|
||||
RestrictAddressFamilies = [ "AF_UNIX" ];
|
||||
RemoveIPC = true;
|
||||
ProtectClock = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
SystemCallErrorNumber = "EPERM";
|
||||
KeyringMode = "private";
|
||||
};
|
||||
in
|
||||
{
|
||||
# --- Audio ---
|
||||
@@ -100,27 +182,6 @@ in
|
||||
# --- Firewall, replaces the ufw calls in post-install.sh ---
|
||||
networking.firewall.enable = true;
|
||||
|
||||
# --- Session services ---
|
||||
#
|
||||
# Rebuilt rather than copied: the upstream units reference /usr/bin/,
|
||||
# which does not exist under Nix.
|
||||
|
||||
systemd.user.services.kanshi = {
|
||||
description = "Dynamic display configuration";
|
||||
partOf = [ "graphical-session.target" ];
|
||||
after = [ "graphical-session.target" ];
|
||||
wantedBy = [ "graphical-session.target" ];
|
||||
# Without a config kanshi exits with a parse error and, with
|
||||
# Restart=on-failure, spins. Monitor profiles are per machine.
|
||||
unitConfig.ConditionPathExists = "%h/.config/kanshi/config";
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = lib.getExe pkgs.kanshi;
|
||||
Restart = "on-failure";
|
||||
RestartSec = 3;
|
||||
};
|
||||
};
|
||||
|
||||
# --- System services from defaults/etc/systemd/system/ ---
|
||||
#
|
||||
# Rebuilt rather than copied, same reason as the user units: the originals
|
||||
@@ -137,15 +198,7 @@ in
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${moonarchPkgs.moonarch-scripts}/bin/moonarch-batsaver-restore";
|
||||
NoNewPrivileges = true;
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
LockPersonality = true;
|
||||
};
|
||||
} // sysfsWriterHardening;
|
||||
};
|
||||
|
||||
systemd.services.moonarch-camera = {
|
||||
@@ -156,14 +209,43 @@ in
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${moonarchPkgs.moonarch-scripts}/bin/moonarch-camera-boot";
|
||||
NoNewPrivileges = true;
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
LockPersonality = true;
|
||||
} // sysfsWriterHardening;
|
||||
};
|
||||
|
||||
# --- Session services ---
|
||||
#
|
||||
# Rebuilt rather than copied: the upstream units reference /usr/bin/,
|
||||
# which does not exist under Nix.
|
||||
|
||||
# The bar, launcher and every popout. A unit rather than a spawn-at-startup so
|
||||
# a crash does not leave the session without a bar until the next login.
|
||||
systemd.user.services.quickshell = {
|
||||
description = "Quickshell bar and shell";
|
||||
partOf = [ "graphical-session.target" ];
|
||||
after = [ "graphical-session.target" ];
|
||||
wantedBy = [ "graphical-session.target" ];
|
||||
# The widgets shell out to niri, nmcli, powerprofilesctl, the moonarch
|
||||
# scripts and a dozen more, and which ones is a property of the QML, not of
|
||||
# this file. As a spawn-at-startup it inherited the session PATH; the unit
|
||||
# gets the same set rather than a list that goes stale on the next widget.
|
||||
path = [ config.system.path ];
|
||||
# Restart=on-failure alone spun 32 times through a dead Wayland socket after
|
||||
# a rebuild had sent niri SIGTERM: graphical-session-pre.target went down,
|
||||
# graphical-session.target stayed up, so the unit was still wanted and every
|
||||
# attempt died on "no Qt platform plugin could be initialized". Quickshell
|
||||
# refuses a relaunch itself ("crashed within 10 seconds"), but systemd never
|
||||
# reached the default limit of 5 starts per 10s at a 3s delay. Three tries
|
||||
# per 30s ends it after ~9s. The window slides, so a single crash after a
|
||||
# normal run still gets its restart — the point of running this as a unit.
|
||||
unitConfig = {
|
||||
StartLimitIntervalSec = 30;
|
||||
StartLimitBurst = 3;
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${lib.getExe pkgs.quickshell} -c moonarch";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 3;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -174,16 +256,41 @@ in
|
||||
partOf = [ "graphical-session.target" ];
|
||||
after = [ "graphical-session.target" ];
|
||||
wantedBy = [ "graphical-session.target" ];
|
||||
# stasis runs its actions as `sh -lc <command>` and spawns niri and pactl
|
||||
# directly, all resolved against this PATH. pulseaudio is here for pactl
|
||||
# alone; it stays off systemPackages, see modules/desktop.nix.
|
||||
path = with pkgs; [ bash swaylock brightnessctl niri pulseaudio ];
|
||||
# Every binary a configured action names has to be listed here. stasis does
|
||||
# not run the actions through a login shell, so /etc/profile never applies
|
||||
# and nothing resolves via /run/current-system/sw/bin. A missing binary is
|
||||
# silent: the spawn fails, stasis logs the action as started, sees the
|
||||
# immediate exit, treats the step as finished and stops there — which is how
|
||||
# swaylock was absent for the whole idle chain while `niri msg` worked.
|
||||
# pactl is why pulseaudio appears at all — it stays off systemPackages, see
|
||||
# modules/desktop.nix. brightnessctl is for the laptop's brightness step.
|
||||
path = with pkgs; [
|
||||
bashNonInteractive
|
||||
niri
|
||||
pulseaudio
|
||||
libnotify
|
||||
swaylock
|
||||
brightnessctl
|
||||
systemd
|
||||
];
|
||||
# No waylandClientHardening: swaylock is a child of this unit and
|
||||
# authenticates through the setuid-root unix_chkpwd. `systemd --user` builds
|
||||
# mount namespaces through an unprivileged user namespace, whose mounts are
|
||||
# nosuid — the wrapper loses setuid and the session is no longer unlockable.
|
||||
# Measured per directive with `echo wrong | unix_chkpwd $USER nonull`: exit
|
||||
# 7 means shadow was read, 9 means no access. Only these five return 7, so
|
||||
# only they are set. 9.4 → 9.0; there is no more to get here.
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${lib.getExe pkgs.stasis}";
|
||||
ExecReload = "${lib.getExe pkgs.stasis} reload";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 2;
|
||||
IPAddressDeny = "any";
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
RemoveIPC = true;
|
||||
KeyringMode = "private";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -197,7 +304,7 @@ in
|
||||
ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --watch ${pkgs.cliphist}/bin/cliphist -db-path %t/cliphist/db store";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 3;
|
||||
};
|
||||
} // waylandClientHardening;
|
||||
};
|
||||
|
||||
systemd.user.services.cliphist-image = {
|
||||
@@ -210,7 +317,7 @@ in
|
||||
ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --type image --watch ${pkgs.cliphist}/bin/cliphist -db-path %t/cliphist/db store";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 3;
|
||||
};
|
||||
} // waylandClientHardening;
|
||||
};
|
||||
|
||||
# Night light stays disabled by default: it is a user toggle. Enabling it
|
||||
@@ -219,7 +326,7 @@ in
|
||||
systemd.user.services.wlsunset = {
|
||||
description = "Wlsunset night light (blue light filter)";
|
||||
partOf = [ "graphical-session.target" ];
|
||||
after = [ "graphical-session.target" "kanshi.service" ];
|
||||
after = [ "graphical-session.target" ];
|
||||
wantedBy = [ ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
@@ -227,7 +334,7 @@ in
|
||||
ExecStart = "${lib.getExe pkgs.wlsunset} -T 6500 -t 5000 -S 00:00 -s 00:01";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 3;
|
||||
};
|
||||
} // waylandClientHardening;
|
||||
};
|
||||
|
||||
# --- Flake age ---
|
||||
|
||||
+2
-2
@@ -25,11 +25,11 @@ let
|
||||
"base_keymap": "SublimeText",
|
||||
"cursor_shape": "block",
|
||||
"ui_font_size": 14,
|
||||
"ui_font_family": "Hack Nerd Font",
|
||||
"ui_font_family": "UbuntuSans Nerd Font",
|
||||
"ui_font_features": {
|
||||
"calt": true
|
||||
},
|
||||
"buffer_font_family": "Hack Nerd Font",
|
||||
"buffer_font_family": "UbuntuSansMono Nerd Font",
|
||||
"buffer_font_size": 14,
|
||||
"theme": {
|
||||
"mode": "system",
|
||||
|
||||
Reference in new issue
Block a user