- Nix 56.1%
- QML 38.1%
- Shell 4.4%
- Python 1.4%
users.defaultUserShell covers root, so shipping fish as the login shell gave root fish as well — found by deploying it and having "ssh root@host 'a=1'" come back as a fish syntax error. Root is the account you reach for when the machine is broken, and it wants the opposite things from a login shell: POSIX syntax that every script and one-liner assumes, no plugins, no prompt, nothing that has to build before you can get a prompt at all. A login shell is a taste; root's shell is a recovery tool. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UxYTTeGcEEEioi5mNXcgnN |
||
|---|---|---|
| dev | ||
| docs | ||
| nix | ||
| shell | ||
| templates/default | ||
| .envrc | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| NOTES.md | ||
| PLAN.md | ||
| README.md | ||
| STYLE.md | ||
fjord
A Linux distribution built on NixOS, with niri as its compositor.
fjord is the system, not an addition to one. It decides the compositor and how it behaves, the keys, the look, the lock screen, the launcher, the login, the firewall, nix policy, boot policy and how applications are declared — the way a distribution decides those things. What is left in a fjord machine's own configuration is its hardware and who it belongs to, and that is close to literally all of it.
NixOS is the substrate rather than the thing you are running, the same way Arch is the substrate under Omarchy. You get NixOS's guarantees for free — every update atomic, every version a generation you can boot back into — without having to write a NixOS configuration to get a desktop.
It looks like a well-set terminal: IBM Plex Mono at every level, dense readouts, a straight value edge, and colour almost never. Terminal typography, not terminal decoration — nothing is drawn with ASCII art, because a frame pinned to a character grid loses every argument it has with a compositor that sizes the window. The reasoning is STYLE.md.
Installing
There is no fjord ISO yet — that is M14 — so you boot NixOS's own installer, partition and mount as usual, and then:
nixos-generate-config --root /mnt
cd /mnt/etc/nixos
nix flake init -t git+https://code.bas.es/bases/fjord.git
That writes four files. Edit the three names at the top of flake.nix —
hostname, username, architecture — and install:
nixos-install --flake /mnt/etc/nixos#<hostname>
The account has no password until you give it one: nixos-enter, then
passwd <username>. Reboot into a greeter, log in, and press Mod+G.
The template's own README.md covers the rest. Answering questions and
partitioning for you is M15; today the NixOS installer does that
part.
What you get
Mod+G opens the status space — a workspace holding nothing but fjord's own
columns, five of them, each a fixed 380 logical pixels wide so a bigger screen
shows more of them rather than wider ones:
| glance | time, date, user, wm, space, battery, network, audio |
| notifications | the live feed, and everything still held |
| system | machine state |
| agenda | today |
| settings | the interactive column |
They are ordinary niri windows, and fjord draws no focus indication on them at all: the compositor already has an answer, so the columns are marked focused however every other window is. That is the rule fjord follows everywhere — where the compositor has an answer, take it, and draw only what it cannot.
Every other workspace carries a 32px rail instead, which says the same things as marks rather than words: a dot per workspace, a battery column that fills from the bottom, wifi bars, a two-row clock. A panel is something you go and read; the rail is something you catch out of the corner of your eye.
Underneath that: niri configured whole — the full keybind set, the layout, the
springs, the palette on the focus ring — plus a lock screen, idle handling,
notifications, a launcher, a clipboard history, the greeter, portals, audio,
GTK and Qt themed dark, a firewall, weekly garbage collection, and a bounded
journal. Mod+Shift+/ lists every key.
What stays yours
A fjord machine's configuration is short, and this is the whole of what is in it:
hardware-configuration.nixand the disk layout- the boot loader and the timezone
- your outputs, keyboard and touchpad
- who you are — a username and a hostname, the two values fjord refuses to guess, so leaving either unset is an evaluation error rather than a machine that quietly belongs to somebody else
- which applications you want on it
- anything you decide fjord got wrong
That last line is not a courtesy. Every opinion fjord holds is a mkDefault,
because when a distribution supplies most of your configuration, a default you
cannot override is not a strong opinion — it is a bug.
Changing fjord's mind
fjord invents no names for things NixOS already has, so disagreeing with it means setting the ordinary option:
# the look
programs.niri.settings.layout.gaps = 8;
programs.fjord.theme.fontSize = 15; # type was chosen against a HiDPI panel
# one key — binds are an attrset, so yours merges with fjord's
programs.niri.settings.binds."Mod+T".action.spawn = "alacritty";
# system policy — no fjord name to learn, just the NixOS option
nix.gc.dates = "daily";
services.journald.extraConfig = "SystemMaxUse=2G";
# reaching the machine: off by default, correct when switched on
programs.fjord.remote.ssh.enable = true; # key-only, no root; opens 22
programs.fjord.remote.tailscale.enable = true; # direct connections, not relayed
programs.fjord.remote.mdns.enable = true; # <hostname>.local actually resolves
# all of fjord's taste, keeping only what the shell needs to work
programs.fjord.niri.defaults = false;
The rule the system side follows: intent is yours, mechanism is fjord's. Whether this machine listens on the network is a fact about you; what a correct sshd looks like is a fact about NixOS. fjord answers the second and leaves the first alone — so sshd is off and already hardened, and unfree packages just evaluate instead of teaching you why they would not.
Colours are the one exception: not tunable, because a palette is an identity rather than a preference.
Declaring an application
One statement, five outputs that cannot disagree with each other:
programs.fjord.workspaces = [ "web" ];
programs.fjord.apps.browser = {
package = pkgs.firefox;
appId = "^firefox$";
workspace = "web";
bind = "Mod+B";
};
That installs it, writes the window rule that catches its windows, creates the workspace they open on, binds the key, and puts it in the launcher. Doing the same by hand means four files that have to agree, and every way of getting it wrong is silent — a mistyped app-id matches nothing and the window opens where it always did; an undeclared workspace makes the rule a no-op. Here the workspace is checked at evaluation, the rule is generated from the same declaration the launcher runs, and the bind spawns a store path rather than a hopeful name.
Already running NixOS?
You do not have to reinstall. fjord's profiles are ordinary NixOS modules, and
taking a slice is supported — import fjord.nixosModules.desktop alone if nix
policy, the login and the firewall are already somebody's job, or
nixosModules.default for all of it:
fjord.nixosModules.default
programs.fjord.enable = true; # the shell
programs.fjord.desktop.enable = true; # compositor, greeter, portals, audio
programs.fjord.system.enable = true; # nix policy, the login, networking
programs.fjord.identity.username = "ada";
programs.fjord.identity.hostName = "kestrel";
All of it is one option surface, on the system side. fjord's home-manager module
declares nothing and is imported for the files it writes — the shell's user
service, niri's config, GTK and Qt — reading everything above through
osConfig. That means home-manager has to run as a NixOS module, which was
already true: niri's own settings tree is only in scope that way.
What it costs
Honestly, and in the order you will meet it:
nixos-unstable. fjord needs Quickshell 0.3.0, which the stable channel does not have.- niri. fjord is not portable to another compositor and is not trying to be. A scrollable-tiling workspace that fjord holds row 1 of is not a theme.
- Three columns are not finished.
glanceandnotificationsread real data.systemreports what niri tells it and not yet cpu, memory or disk;agendahas no data source decided;settingsis placeholders for shape. - No releases yet. No tags and no stable channel, so pinning means pinning a commit.
- No screen reader. niri's stock config binds one; fjord does not ship orca, and that is a gap rather than a decision anyone should be happy with.
Where it is
| PLAN.md | what fjord is, why the boundary sits where it does, and the milestones |
| STYLE.md | the visual language and the reasoning behind it |
| NOTES.md | the audit trail — what was verified, on what, and what surprised us |
nix build .#docs |
the documentation site, with every option generated from the module |
Status
Working: the compositor and its whole configuration, the five columns,
notifications, launcher, clipboard, lock and idle, the greeter, portals, audio,
GTK/Qt theming, programs.fjord.apps, the system profile, and remote access.
Outstanding: real data in three columns (M5), multi-output (M7), a live ISO (M14), an installer (M15), releases and an update command (M17), the terminal half — shell, editor, multiplexer — and a settings surface (M20).
fjord is developed against two machines and installed on a third that is nothing but a consumer of it, with no configuration around it to paper over what fjord forgets to ship. Most of what this repo knows was found that way rather than designed.