fix(mibook): boot-menu terminal choice via specialisation

The previous startOnBoot approach booted mibook headless by default and
locked the machine out: it is WiFi-only with credentials in KWallet, so
with no desktop session NetworkManager never joins the network (no SSH),
and boot stalled on NetworkManager-wait-online with the tty1 prompt
buried under service logs.

Replace it with a NixOS specialisation that adds a separate 'terminal'
GRUB entry:
- default entry boots KDE as before (identical to baseline);
- 'terminal' entry boots multi-user.target with autologin for finn and a
  'desktop' command to start SDDM on demand.
Also disable NetworkManager-wait-online so boot never stalls on the
network. Revert the kde-desktop startOnBoot option.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CfozKLQdUh5TzqyjSigLUx
This commit is contained in:
2026-07-27 10:52:18 +02:00
parent f01b1f2c4f
commit ae6c81bf54
4 changed files with 127 additions and 75 deletions
@@ -2,60 +2,104 @@
## Goal ## Goal
Let mibook boot to a text console by default, with KDE fully installed and Let mibook offer a choice, each time it boots, between the normal KDE desktop
launchable on demand. The user decides after boot whether they need the and a terminal-only ("server") mode with no graphical session — decided at
graphical desktop, without rebuilding the system. boot, without rebuilding the system.
## Background: why the first attempt failed
The first implementation booted mibook to a text console by default
(`systemd.defaultUnit = "multi-user.target"`, display manager not started) and
provided a `desktop` command to start KDE on demand. On real hardware this
locked the machine out:
- **No SSH.** mibook is WiFi-only and its WiFi credentials are stored per-user
in KWallet ("agent-owned"). NetworkManager only receives the password once a
desktop session is running, so a headless boot never joins the network and
the machine has no IP — nothing to SSH into.
- **No usable console.** The boot appeared to "hang" with no login prompt:
`NetworkManager-wait-online` stalled waiting for a network that never came
up, and the `getty` login prompt on tty1 was buried under later service
messages.
Conclusion: a headless WiFi laptop cannot be reached remotely, and the plain
console was hard to use. The design must (a) keep the desktop as the reliable
default, (b) make the terminal path a deliberate, self-sufficient choice, and
(c) not depend on the network being up.
## Behavior ## Behavior
- mibook boots to a text console (`multi-user.target`). SDDM and Plasma do - The **default GRUB entry** boots straight into KDE — unchanged from the
**not** start automatically. known-working baseline.
- KDE (SDDM + Plasma 6) remains fully installed. - A **separate GRUB entry, `mibook (terminal)`** (a NixOS *specialisation*),
- Running `desktop` in the terminal starts the SDDM display manager, which boots to a text console with **autologin** for `finn`. From there the user
presents the graphical login screen; logging in there enters a Plasma 6 can work in the shell or run `desktop` to bring KDE up (via SDDM).
session. - The choice is made in the GRUB menu at boot — matching the original request
- No reverse command. To return to a text console, stop the display manager to "decide each time I boot."
(`sudo systemctl stop display-manager`) or reboot. - Booting never stalls on the network.
- Other machines that enable the `kde-desktop` profile are unaffected; their
desktop still starts on boot. ### Known limitation (documented, not fixed in config)
In terminal mode WiFi will not connect on its own, because the password is
stored per-user in KWallet. To reach mibook over SSH from terminal mode, the
user must first save the WiFi as a **system** connection in KDE:
network settings → the WiFi network → "All users may connect to this network".
Until then, terminal mode is local-console-only. This is a one-time manual
step outside the scope of the Nix config.
## Implementation ## Implementation
### `modules/environments/kde-desktop/default.nix` ### `machines/mibook/configuration.nix`
Add an opt-out option to the existing profile: - Add a NixOS specialisation `specialisation.terminal.configuration`:
- `system.nixos.tags = [ "terminal" ];` — labels the generated boot entry.
- `systemd.defaultUnit = lib.mkForce "multi-user.target";` — boots to the
text console. `graphical.target` is what pulls in the display manager (via
its embedded `Wants=display-manager.service`), so defaulting to
`multi-user.target` leaves SDDM installed but not started at boot.
- `services.getty.autologinUser = "finn";` — guarantees a usable shell on
the console instead of a login prompt that can scroll off screen.
- A `desktop` command via
`pkgs.writeShellScriptBin "desktop" "exec sudo systemctl start display-manager.service"`
in `environment.systemPackages`, to start KDE on demand.
- Add `systemd.services.NetworkManager-wait-online.enable = false;` (applies to
both the default and terminal boots) so boot never stalls waiting for the
network.
- New option `my.profiles.kde-desktop.startOnBoot`, a boolean defaulting to ### Reverted from the first attempt
`true`. The default preserves the current behavior for every consumer.
- The existing `config` block (SDDM, Plasma 6, user packages) stays enabled
whenever the profile is enabled, regardless of `startOnBoot`.
- When `startOnBoot = false`, additionally apply:
- `systemd.services.display-manager.wantedBy = lib.mkForce [ ];` so SDDM is
defined but not pulled in by any boot target.
- `systemd.defaultUnit = "multi-user.target";` so the boot target is the
text console.
- A `desktop` command provided via
`pkgs.writeShellScriptBin "desktop" "exec sudo systemctl start display-manager.service"`,
added to `environment.systemPackages`.
### `machines/mibook/environments.nix` - `modules/environments/kde-desktop/default.nix` — remove the `startOnBoot`
option and its `mkMerge`/`mkIf` machinery; back to the original profile that
simply enables SDDM + Plasma 6.
- `machines/mibook/environments.nix` — remove `kde-desktop.startOnBoot = false;`
(back to just `kde-desktop.enable = true;`).
Set `kde-desktop.startOnBoot = false;` alongside the existing ## Why a specialisation
`kde-desktop.enable = true;`.
A specialisation generates a second boot-menu entry automatically from a
modified copy of the configuration. It is the idiomatic NixOS mechanism for a
boot-time choice and avoids fragile hand-written GRUB `extraEntries` that would
need to track kernel/initrd paths across generations. The default entry remains
byte-for-byte the working desktop configuration.
## Testing / verification ## Testing / verification
- `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'` - `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'`
builds without error. builds both `nixos-system-mibook` and `nixos-system-mibook-terminal`.
- After `nixos-rebuild switch` on mibook: system boots to a TTY, no SDDM; - Verified on the built closures:
running `desktop` brings up the SDDM login and a working Plasma session. - Parent `default.target``graphical.target`; no console autologin
- Confirm jupiter (or any non-mibook consumer) is unchanged: its evaluated (identical to the pre-change baseline).
`systemd.defaultUnit` / display-manager wantedBy are not altered. - Specialisation `default.target``multi-user.target`; tty1 getty wrapper
contains `--autologin finn`; `desktop` present in the system profile.
- `NetworkManager-wait-online` disabled in both.
- Post-`switch` manual check on mibook: default GRUB entry boots to KDE; the
`terminal` entry boots to an autologged-in console; running `desktop` there
starts SDDM and a working Plasma session.
## Trade-offs ## Trade-offs
- `desktop` relies on `sudo`; the user has sudo access, so no extra - Autologin on the terminal console means physical access grants a shell
configuration is required. without a password. Acceptable for a personal laptop the user controls; the
- Chose "boot to terminal, launch on demand" over a GRUB boot-menu entry desktop (default) boot is unaffected.
because it is more robust across NixOS generations and needs no fragile - `desktop` relies on `sudo`; the user has sudo access, so no extra config is
static kernel entries. required.
+31 -2
View File
@@ -20,7 +20,6 @@
useOSProber = true; useOSProber = true;
}; };
# Configure keymap in X11 # Configure keymap in X11
services.xserver.xkb = { services.xserver.xkb = {
layout = "de"; layout = "de";
@@ -34,7 +33,6 @@
services.printing.enable = true; services.printing.enable = true;
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
hardware.nvidia.prime = { hardware.nvidia.prime = {
sync.enable = false; sync.enable = false;
@@ -44,6 +42,37 @@
services.openssh.enable = true; services.openssh.enable = true;
# Don't let boot stall waiting for a network that may never come up
# (WiFi credentials live in KWallet and need a desktop session), which
# otherwise hangs the terminal boot before the login prompt appears.
systemd.services.NetworkManager-wait-online.enable = false;
# Boot-time choice: the default GRUB entry boots straight into KDE.
# A separate "terminal" entry (a NixOS specialisation) boots to a text
# console with autologin, where you can work or run `desktop` to bring
# KDE up. Pick the entry you want in the GRUB menu at boot.
#
# NOTE: in terminal mode WiFi will not connect on its own (the password
# is stored per-user in KWallet). To reach the machine over SSH from
# terminal mode, first save the WiFi as a system connection in KDE:
# network settings -> your WiFi -> "All users may connect to this network".
specialisation.terminal.configuration = {
system.nixos.tags = [ "terminal" ];
# Boot to a text console. graphical.target is what pulls in the display
# manager (via its embedded Wants=display-manager.service), so defaulting
# to multi-user.target leaves SDDM installed but not started at boot.
systemd.defaultUnit = lib.mkForce "multi-user.target";
# Guarantee a usable shell on the console (no login prompt to hunt for).
services.getty.autologinUser = "finn";
# Bring the desktop up on demand from the terminal.
environment.systemPackages = [
(pkgs.writeShellScriptBin "desktop" "exec sudo systemctl start display-manager.service")
];
};
# KDE (PowerDevil) power settings: do nothing on lid close while on AC power. # KDE (PowerDevil) power settings: do nothing on lid close while on AC power.
# Shipped as a system-wide default; KConfig cascades so a user's own # Shipped as a system-wide default; KConfig cascades so a user's own
# ~/.config/powerdevilrc will override this if present. # ~/.config/powerdevilrc will override this if present.
-1
View File
@@ -6,7 +6,6 @@ in
{ {
my.profiles = { my.profiles = {
kde-desktop.enable = true; kde-desktop.enable = true;
kde-desktop.startOnBoot = false;
zsh.enable = true; zsh.enable = true;
apps = { apps = {
desktop_apps = true; desktop_apps = true;
+12 -32
View File
@@ -11,37 +11,17 @@ in
{ {
options.my.profiles.kde-desktop = with lib; { options.my.profiles.kde-desktop = with lib; {
enable = mkEnableOption "KDE Desktop Environment"; enable = mkEnableOption "KDE Desktop Environment";
startOnBoot = mkOption {
type = types.bool;
default = true;
description = ''
Whether the display manager (SDDM) starts automatically at boot.
When false, the system boots to a text console and KDE can be
launched on demand with the `desktop` command.
'';
};
}; };
config = lib.mkIf cfg.enable (lib.mkMerge [ config = lib.mkIf cfg.enable {
{ services = {
services = { displayManager.sddm.enable = true;
displayManager.sddm.enable = true; displayManager.sddm.wayland.enable = true;
displayManager.sddm.wayland.enable = true; desktopManager.plasma6.enable = true;
desktopManager.plasma6.enable = true; };
}; users.users.finn.packages = with pkgs; [
users.users.finn.packages = with pkgs; [ # Programms can be added here...
# Programms can be added here... numix-icon-theme
numix-icon-theme ];
]; };
} }
(lib.mkIf (!cfg.startOnBoot) {
# Boot to a text console; SDDM stays installed but is not pulled in
# by any boot target. Launch KDE on demand with `desktop`.
systemd.services.display-manager.wantedBy = lib.mkForce [ ];
systemd.defaultUnit = lib.mkForce "multi-user.target";
environment.systemPackages = [
(pkgs.writeShellScriptBin "desktop" "exec sudo systemctl start display-manager.service")
];
})
]);
}