feat: keymap/locale + form factor in installer; nm-applet visible by default

- Installer prompts for keyboard layout (with optional variant) and locale
  via curated short list + Other… fallback into the full localectl list;
  applies to the live session immediately (loadkeys + hyprctl) so the
  rest of the install types correctly. Generated system.nix emits
  console.keyMap, i18n.defaultLocale, and services.xserver.xkb.{layout,
  variant}.
- New nomarchy.{system,}.formFactor enum (laptop|desktop, default laptop).
  Installer auto-detects via /sys/class/power_supply/BAT* and lets the
  user flip the answer. Waybar drops the battery widget on desktop;
  battery-monitor service is gated on the same option.
- Lift waybar tray out of the collapsed group/tray-expander in the default
  theme so nm-applet's icon is visible without expanding the drawer.
- Live ISOs (TTY + graphical) get baseline mkDefault keyMap/locale so the
  installer's runtime override always wins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-04-25 20:26:55 +01:00
parent 7fd0f78d7c
commit a7e7fa9562
8 changed files with 203 additions and 29 deletions

View File

@@ -44,6 +44,18 @@
default = "summer-night"; default = "summer-night";
description = "System theme name."; description = "System theme name.";
}; };
formFactor = lib.mkOption {
type = lib.types.enum [ "laptop" "desktop" ];
default = "laptop";
description = ''
Physical form factor. Drives UI affordances (battery widget,
future lid handling). Default "laptop" battery widget is
harmless on a desktop (renders empty when no BAT* is present),
so the safe default is "show, don't hide". The installer
auto-detects via /sys/class/power_supply/BAT* and writes the
explicit value into the generated home.nix.
'';
};
wallpaper = lib.mkOption { wallpaper = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = ""; default = "";

View File

@@ -24,6 +24,18 @@
default = "UTC"; default = "UTC";
description = "System timezone."; description = "System timezone.";
}; };
formFactor = lib.mkOption {
type = lib.types.enum [ "laptop" "desktop" ];
default = "laptop";
description = ''
Physical form factor. Drives UI affordances (battery widget,
future lid handling / TLP). Default "laptop" battery widget
is harmless on a desktop (renders empty when no BAT* is
present), so the safe default is "show, don't hide". The
installer auto-detects via /sys/class/power_supply/BAT* and
writes the explicit value into the generated system.nix.
'';
};
features = { features = {
fingerprint = lib.mkOption { fingerprint = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;

View File

@@ -7,7 +7,7 @@
"modules-left": ["custom/nomarchy", "hyprland/workspaces"], "modules-left": ["custom/nomarchy", "hyprland/workspaces"],
"modules-center": ["clock", "custom/update", "custom/voxtype", "custom/screenrecording-indicator", "custom/idle-indicator", "custom/notification-silencing-indicator"], "modules-center": ["clock", "custom/update", "custom/voxtype", "custom/screenrecording-indicator", "custom/idle-indicator", "custom/notification-silencing-indicator"],
"modules-right": [ "modules-right": [
"group/tray-expander", "tray",
"bluetooth", "bluetooth",
"network", "network",
"pulseaudio", "pulseaudio",
@@ -120,22 +120,6 @@
"default": ["", "", ""] "default": ["", "", ""]
} }
}, },
"group/tray-expander": {
"orientation": "inherit",
"drawer": {
"transition-duration": 600,
"children-class": "tray-group-item"
},
"modules": ["custom/expand-icon", "tray"]
},
"custom/expand-icon": {
"format": "",
"tooltip": false,
"on-scroll-up": "",
"on-scroll-down": "",
"on-scroll-left": "",
"on-scroll-right": ""
},
"custom/screenrecording-indicator": { "custom/screenrecording-indicator": {
"on-click": "nomarchy-cmd-screenrecord", "on-click": "nomarchy-cmd-screenrecord",
"exec": "~/.config/nomarchy/default/waybar/indicators/screen-recording.sh", "exec": "~/.config/nomarchy/default/waybar/indicators/screen-recording.sh",

View File

@@ -15,14 +15,31 @@ let
# Selected files # Selected files
configFile = if hasThemeConfig then (themeDir + "/config.jsonc") else defaultConfig; configFile = if hasThemeConfig then (themeDir + "/config.jsonc") else defaultConfig;
styleFile = if hasThemeStyle then (themeDir + "/style.css") else defaultStyle; styleFile = if hasThemeStyle then (themeDir + "/style.css") else defaultStyle;
rawSettings = builtins.fromJSON (builtins.readFile configFile);
# Modules that only make sense on a laptop. Filtered out of any
# `modules-*` slot when nomarchy.formFactor != "laptop" so a desktop
# build doesn't ship a permanently-empty battery indicator.
laptopOnlyModules = [ "battery" "custom/battery" ];
filterModules = mods:
if config.nomarchy.formFactor == "laptop"
then mods
else builtins.filter (m: !(builtins.elem m laptopOnlyModules)) mods;
settings = rawSettings // {
modules-left = filterModules (rawSettings.modules-left or []);
modules-center = filterModules (rawSettings.modules-center or []);
modules-right = filterModules (rawSettings.modules-right or []);
};
in in
{ {
programs.waybar = { programs.waybar = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
systemd.enable = lib.mkDefault true; systemd.enable = lib.mkDefault true;
settings = lib.mkDefault [ (builtins.fromJSON (builtins.readFile configFile)) ]; settings = lib.mkDefault [ settings ];
style = lib.mkDefault (builtins.readFile styleFile); style = lib.mkDefault (builtins.readFile styleFile);
}; };

View File

@@ -1,12 +1,12 @@
{ pkgs, ... }: { config, lib, pkgs, ... }:
{ lib.mkIf (config.nomarchy.formFactor == "laptop") {
systemd.user.services.nomarchy-battery-monitor = { systemd.user.services.nomarchy-battery-monitor = {
Unit = { Unit = {
Description = "Nomarchy Battery Monitor Check"; Description = "Nomarchy Battery Monitor Check";
After = [ "graphical-session.target" ]; After = [ "graphical-session.target" ];
# Skip on hosts with no battery (VMs, desktops) — otherwise the # Belt-and-braces: even on a laptop generation, skip if the kernel
# monitor script fails and degrades the user session. # hasn't surfaced a battery yet (e.g. early boot, removable battery).
ConditionPathExistsGlob = "/sys/class/power_supply/BAT*"; ConditionPathExistsGlob = "/sys/class/power_supply/BAT*";
}; };

View File

@@ -12,12 +12,18 @@
# Base installation media configuration is handled by the module imported in flake.nix # Base installation media configuration is handled by the module imported in flake.nix
# Console configuration for a pleasant TTY experience # Console configuration for a pleasant TTY experience.
# keyMap and i18n.defaultLocale are mkDefault — the install.sh keymap step
# calls `loadkeys` at runtime to honor the user's pick during the install,
# and writes the chosen value into the *installed* system's system.nix.
console = { console = {
font = "ter-v16n"; font = "ter-v16n";
packages = [ pkgs.terminus_font ]; packages = [ pkgs.terminus_font ];
keyMap = lib.mkDefault "us";
}; };
i18n.defaultLocale = lib.mkDefault "en_US.UTF-8";
# Essential packages for installation # Essential packages for installation
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
# Core utilities # Core utilities

View File

@@ -1,6 +1,13 @@
{ config, pkgs, inputs, lib, ... }: { config, pkgs, inputs, lib, ... }:
{ {
# Live-ISO defaults. The install.sh keymap step calls `hyprctl keyword
# input:kb_layout` at runtime so the user's pick takes effect during the
# install, and writes the chosen value into the *installed* system.
console.keyMap = lib.mkDefault "us";
i18n.defaultLocale = lib.mkDefault "en_US.UTF-8";
services.xserver.xkb.layout = lib.mkDefault "us";
# Home Manager activation for the nixos live user is provided by the # Home Manager activation for the nixos live user is provided by the
# home-manager.nixosModules.home-manager module wired up in flake.nix. # home-manager.nixosModules.home-manager module wired up in flake.nix.

View File

@@ -33,11 +33,26 @@ USERNAME=""
LUKS_PASSWORD="" LUKS_PASSWORD=""
USER_PASSWORD="" USER_PASSWORD=""
TIMEZONE="UTC" TIMEZONE="UTC"
KEYMAP_LAYOUT=""
KEYMAP_VARIANT=""
LOCALE=""
FORM_FACTOR=""
HARDWARE_MODULES="" HARDWARE_MODULES=""
NOMARCHY_HW_OPTS="" NOMARCHY_HW_OPTS=""
# "" = not yet answered; "true"/"false" set by configure_impermanence. # "" = not yet answered; "true"/"false" set by configure_impermanence.
ENABLE_IMPERMANENCE="" ENABLE_IMPERMANENCE=""
# Curated short lists for the keymap/locale prompts. "Other…" drops the user
# into the full `localectl` list via gum filter.
COMMON_KEYMAPS=(
us de fr gb es it pt br se no fi dk nl pl ru ua ch jp kr cn ar
)
COMMON_LOCALES=(
en_US.UTF-8 en_GB.UTF-8 de_DE.UTF-8 fr_FR.UTF-8 es_ES.UTF-8
it_IT.UTF-8 pt_BR.UTF-8 pt_PT.UTF-8 nl_NL.UTF-8 sv_SE.UTF-8
ja_JP.UTF-8 zh_CN.UTF-8 ko_KR.UTF-8
)
# CLI flags # CLI flags
DRY_RUN="false" DRY_RUN="false"
RESUME="false" RESUME="false"
@@ -79,6 +94,7 @@ parse_args() {
save_state() { save_state() {
declare -p \ declare -p \
TARGET_DRIVE USERNAME HOSTNAME TIMEZONE \ TARGET_DRIVE USERNAME HOSTNAME TIMEZONE \
KEYMAP_LAYOUT KEYMAP_VARIANT LOCALE FORM_FACTOR \
ENABLE_IMPERMANENCE HARDWARE_MODULES NOMARCHY_HW_OPTS NOMARCHY_REV \ ENABLE_IMPERMANENCE HARDWARE_MODULES NOMARCHY_HW_OPTS NOMARCHY_REV \
> "$STATE_FILE" > "$STATE_FILE"
} }
@@ -320,6 +336,67 @@ configure_user() {
save_state save_state
} }
# ============================================================================
# STEP 5a: KEYBOARD & LANGUAGE
# ============================================================================
# Curated short list first ("Other…" drops into the full localectl list).
# Applied IMMEDIATELY to the running session so the rest of the install types
# correctly — TTY uses loadkeys, Hyprland uses hyprctl. Both best-effort.
select_keymap_locale() {
section "Keyboard & Language"
if [[ -z "$KEYMAP_LAYOUT" ]]; then
local choice
choice=$(printf '%s\n' "${COMMON_KEYMAPS[@]}" "Other…" \
| nrun gum choose --header "Keyboard layout")
if [[ "$choice" == "Other…" ]]; then
KEYMAP_LAYOUT=$(localectl list-x11-keymap-layouts 2>/dev/null \
| nrun gum filter --placeholder "Search keyboard layout…")
else
KEYMAP_LAYOUT="$choice"
fi
[[ -z "$KEYMAP_LAYOUT" ]] && KEYMAP_LAYOUT="us"
fi
success "Keyboard layout: $KEYMAP_LAYOUT"
# Variant — optional. Only prompt if the layout actually has variants.
if [[ -z "$KEYMAP_VARIANT" ]]; then
local variants
variants=$(localectl list-x11-keymap-variants "$KEYMAP_LAYOUT" 2>/dev/null || true)
if [[ -n "$variants" ]]; then
local v
v=$(printf '(none)\n%s\n' "$variants" \
| nrun gum filter --placeholder "Variant (optional)" --value "(none)")
[[ "$v" == "(none)" || -z "$v" ]] || KEYMAP_VARIANT="$v"
fi
fi
[[ -n "$KEYMAP_VARIANT" ]] && success "Variant: $KEYMAP_VARIANT" || success "Variant: (default)"
# Apply to the live session, best-effort.
loadkeys "$KEYMAP_LAYOUT" 2>/dev/null || true
if [[ -n "${WAYLAND_DISPLAY:-}" ]] && command -v hyprctl >/dev/null 2>&1; then
hyprctl keyword input:kb_layout "$KEYMAP_LAYOUT" >/dev/null 2>&1 || true
hyprctl keyword input:kb_variant "$KEYMAP_VARIANT" >/dev/null 2>&1 || true
fi
if [[ -z "$LOCALE" ]]; then
local choice
choice=$(printf '%s\n' "${COMMON_LOCALES[@]}" "Other…" \
| nrun gum choose --header "Language / locale")
if [[ "$choice" == "Other…" ]]; then
LOCALE=$(localectl list-locales 2>/dev/null \
| nrun gum filter --placeholder "Search locale…")
else
LOCALE="$choice"
fi
[[ -z "$LOCALE" ]] && LOCALE="en_US.UTF-8"
fi
success "Locale: $LOCALE"
save_state
}
# ============================================================================ # ============================================================================
# STEP 5: TIMEZONE # STEP 5: TIMEZONE
# ============================================================================ # ============================================================================
@@ -497,6 +574,36 @@ _select_hardware_manual() {
esac esac
} }
# ============================================================================
# STEP 6b: FORM FACTOR (LAPTOP / DESKTOP)
# ============================================================================
# Auto-detects via /sys/class/power_supply/BAT* — same signal hardware-db.sh
# uses to pick common-pc-laptop vs common-pc. The user can flip the answer
# (e.g. a desktop with a UPS that exposes a BAT*, or a laptop that doesn't).
confirm_form_factor() {
section "Form Factor"
if [[ -n "$FORM_FACTOR" ]]; then
success "Resumed: $FORM_FACTOR"
return
fi
local default="desktop"
if compgen -G "/sys/class/power_supply/BAT*" >/dev/null; then
default="laptop"
fi
info "Auto-detected: $default"
if nrun gum confirm "Treat this machine as a $default?"; then
FORM_FACTOR="$default"
else
FORM_FACTOR=$([[ "$default" == "laptop" ]] && echo desktop || echo laptop)
fi
success "Form factor: $FORM_FACTOR"
save_state
}
# ============================================================================ # ============================================================================
# STEP 7: IMPERMANENCE (OPTIONAL) # STEP 7: IMPERMANENCE (OPTIONAL)
# ============================================================================ # ============================================================================
@@ -533,7 +640,10 @@ review_configuration() {
echo " Drive: $TARGET_DRIVE (BTRFS + LUKS2)" echo " Drive: $TARGET_DRIVE (BTRFS + LUKS2)"
echo " Hostname: $HOSTNAME" echo " Hostname: $HOSTNAME"
echo " Username: $USERNAME" echo " Username: $USERNAME"
echo " Keymap: $KEYMAP_LAYOUT${KEYMAP_VARIANT:+ ($KEYMAP_VARIANT)}"
echo " Locale: $LOCALE"
echo " Timezone: $TIMEZONE" echo " Timezone: $TIMEZONE"
echo " Form factor: $FORM_FACTOR"
echo " Impermanence: $ENABLE_IMPERMANENCE" echo " Impermanence: $ENABLE_IMPERMANENCE"
echo " Nomarchy rev: ${NOMARCHY_REV:-main (unpinned)}" echo " Nomarchy rev: ${NOMARCHY_REV:-main (unpinned)}"
echo "" echo ""
@@ -821,12 +931,29 @@ EOF
# system.nix — curated system-level options. Uncomment what you want and # system.nix — curated system-level options. Uncomment what you want and
# run \`sudo nixos-rebuild switch --flake /etc/nixos#$HOSTNAME\` to apply. # run \`sudo nixos-rebuild switch --flake /etc/nixos#$HOSTNAME\` to apply.
# XKB variant is optional — only emit when the user picked one.
local xkb_variant_line=""
if [[ -n "$KEYMAP_VARIANT" ]]; then
xkb_variant_line=" variant = \"$KEYMAP_VARIANT\";"
fi
cat > /mnt/etc/nixos/system.nix << EOF cat > /mnt/etc/nixos/system.nix << EOF
{ pkgs, ... }: { pkgs, ... }:
{ {
networking.hostName = "$HOSTNAME"; networking.hostName = "$HOSTNAME";
time.timeZone = "$TIMEZONE"; time.timeZone = "$TIMEZONE";
# Keyboard & language — set by the installer.
console.keyMap = "$KEYMAP_LAYOUT";
i18n.defaultLocale = "$LOCALE";
services.xserver.xkb = {
layout = "$KEYMAP_LAYOUT";
$xkb_variant_line
};
# Physical form factor — gates UI affordances (battery widget, etc).
nomarchy.system.formFactor = "$FORM_FACTOR";
$impermanence_opt $impermanence_opt
# Compressed RAM swap. Near-free memory headroom on small machines and # Compressed RAM swap. Near-free memory headroom on small machines and
@@ -914,15 +1041,22 @@ EOF
# home.nix — curated app menu. Uncomment what you want and run # home.nix — curated app menu. Uncomment what you want and run
# `nomarchy-env-update` to apply. # `nomarchy-env-update` to apply.
cat > /mnt/etc/nixos/home.nix << 'EOF' #
# NB: not heredoc-quoted — we expand $FORM_FACTOR. Any literal `$` or
# backtick in the body must be escaped.
cat > /mnt/etc/nixos/home.nix << EOF
{ pkgs, ... }: { pkgs, ... }:
{ {
# Physical form factor — mirrors nomarchy.system.formFactor in system.nix.
# Gates UI affordances like the waybar battery widget.
nomarchy.formFactor = "$FORM_FACTOR";
# User-level packages (Home Manager). # User-level packages (Home Manager).
# #
# Nomarchy already ships a minimal desktop (firefox, thunar, mpv, imv, mako, # Nomarchy already ships a minimal desktop (firefox, thunar, mpv, imv, mako,
# hyprlock, swww, wl-clipboard, grim, slurp, rofi-wayland, etc.). The list # hyprlock, swww, wl-clipboard, grim, slurp, rofi-wayland, etc.). The list
# below is a menu of extras — uncomment what you want and run # below is a menu of extras — uncomment what you want and run
# `nomarchy-env-update`. # \`nomarchy-env-update\`.
home.packages = with pkgs; [ home.packages = with pkgs; [
# --- Enabled by default --- # --- Enabled by default ---
btop # Resource monitor (TUI) btop # Resource monitor (TUI)
@@ -989,8 +1123,8 @@ EOF
# --- Optional Nomarchy app modules --- # --- Optional Nomarchy app modules ---
# opencode AI coding CLI integration (deploys ~/.config/opencode/opencode.json). # opencode AI coding CLI integration (deploys ~/.config/opencode/opencode.json).
# The `opencode` package itself is not installed automatically — add it to # The \`opencode\` package itself is not installed automatically — add it to
# `home.packages` above if you want it on PATH. # \`home.packages\` above if you want it on PATH.
# nomarchy.apps.opencode.enable = true; # nomarchy.apps.opencode.enable = true;
# Extra Home Manager modules go here (program configs, services, etc.). # Extra Home Manager modules go here (program configs, services, etc.).
@@ -1041,8 +1175,10 @@ main() {
select_disk select_disk
get_luks_passphrase get_luks_passphrase
configure_user configure_user
select_keymap_locale
select_timezone select_timezone
select_hardware select_hardware
confirm_form_factor
configure_impermanence configure_impermanence
review_configuration review_configuration
execute_installation execute_installation