From a7e7fa95621fc06485717d2157321e4eafe98486 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Sat, 25 Apr 2026 20:26:55 +0100 Subject: [PATCH] feat: keymap/locale + form factor in installer; nm-applet visible by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- core/home/options.nix | 12 ++ core/system/options.nix | 12 ++ features/desktop/waybar/config/config.jsonc | 18 +-- features/desktop/waybar/default.nix | 23 +++- features/scripts/battery-monitor.nix | 8 +- hosts/installer-iso.nix | 8 +- hosts/live-iso.nix | 7 + installer/install.sh | 144 +++++++++++++++++++- 8 files changed, 203 insertions(+), 29 deletions(-) diff --git a/core/home/options.nix b/core/home/options.nix index 509d96f..a6f40fe 100644 --- a/core/home/options.nix +++ b/core/home/options.nix @@ -44,6 +44,18 @@ default = "summer-night"; 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 { type = lib.types.str; default = ""; diff --git a/core/system/options.nix b/core/system/options.nix index 9b3e5b6..757dfa7 100644 --- a/core/system/options.nix +++ b/core/system/options.nix @@ -24,6 +24,18 @@ default = "UTC"; 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 = { fingerprint = lib.mkOption { type = lib.types.bool; diff --git a/features/desktop/waybar/config/config.jsonc b/features/desktop/waybar/config/config.jsonc index d7329ff..ebf4ac0 100644 --- a/features/desktop/waybar/config/config.jsonc +++ b/features/desktop/waybar/config/config.jsonc @@ -7,7 +7,7 @@ "modules-left": ["custom/nomarchy", "hyprland/workspaces"], "modules-center": ["clock", "custom/update", "custom/voxtype", "custom/screenrecording-indicator", "custom/idle-indicator", "custom/notification-silencing-indicator"], "modules-right": [ - "group/tray-expander", + "tray", "bluetooth", "network", "pulseaudio", @@ -120,22 +120,6 @@ "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": { "on-click": "nomarchy-cmd-screenrecord", "exec": "~/.config/nomarchy/default/waybar/indicators/screen-recording.sh", diff --git a/features/desktop/waybar/default.nix b/features/desktop/waybar/default.nix index 6e872f8..094ef59 100644 --- a/features/desktop/waybar/default.nix +++ b/features/desktop/waybar/default.nix @@ -15,14 +15,31 @@ let # Selected files configFile = if hasThemeConfig then (themeDir + "/config.jsonc") else defaultConfig; 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 { programs.waybar = { 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); }; diff --git a/features/scripts/battery-monitor.nix b/features/scripts/battery-monitor.nix index 9b5aa66..035f989 100644 --- a/features/scripts/battery-monitor.nix +++ b/features/scripts/battery-monitor.nix @@ -1,12 +1,12 @@ -{ pkgs, ... }: +{ config, lib, pkgs, ... }: -{ +lib.mkIf (config.nomarchy.formFactor == "laptop") { systemd.user.services.nomarchy-battery-monitor = { Unit = { Description = "Nomarchy Battery Monitor Check"; After = [ "graphical-session.target" ]; - # Skip on hosts with no battery (VMs, desktops) — otherwise the - # monitor script fails and degrades the user session. + # Belt-and-braces: even on a laptop generation, skip if the kernel + # hasn't surfaced a battery yet (e.g. early boot, removable battery). ConditionPathExistsGlob = "/sys/class/power_supply/BAT*"; }; diff --git a/hosts/installer-iso.nix b/hosts/installer-iso.nix index cffae1f..1c9cb62 100644 --- a/hosts/installer-iso.nix +++ b/hosts/installer-iso.nix @@ -12,12 +12,18 @@ # 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 = { font = "ter-v16n"; packages = [ pkgs.terminus_font ]; + keyMap = lib.mkDefault "us"; }; + i18n.defaultLocale = lib.mkDefault "en_US.UTF-8"; + # Essential packages for installation environment.systemPackages = with pkgs; [ # Core utilities diff --git a/hosts/live-iso.nix b/hosts/live-iso.nix index 7a87e48..42a1aa4 100644 --- a/hosts/live-iso.nix +++ b/hosts/live-iso.nix @@ -1,6 +1,13 @@ { 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.nixosModules.home-manager module wired up in flake.nix. diff --git a/installer/install.sh b/installer/install.sh index 12ec998..da24ccd 100755 --- a/installer/install.sh +++ b/installer/install.sh @@ -33,11 +33,26 @@ USERNAME="" LUKS_PASSWORD="" USER_PASSWORD="" TIMEZONE="UTC" +KEYMAP_LAYOUT="" +KEYMAP_VARIANT="" +LOCALE="" +FORM_FACTOR="" HARDWARE_MODULES="" NOMARCHY_HW_OPTS="" # "" = not yet answered; "true"/"false" set by configure_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 DRY_RUN="false" RESUME="false" @@ -79,6 +94,7 @@ parse_args() { save_state() { declare -p \ TARGET_DRIVE USERNAME HOSTNAME TIMEZONE \ + KEYMAP_LAYOUT KEYMAP_VARIANT LOCALE FORM_FACTOR \ ENABLE_IMPERMANENCE HARDWARE_MODULES NOMARCHY_HW_OPTS NOMARCHY_REV \ > "$STATE_FILE" } @@ -320,6 +336,67 @@ configure_user() { 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 # ============================================================================ @@ -497,6 +574,36 @@ _select_hardware_manual() { 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) # ============================================================================ @@ -533,7 +640,10 @@ review_configuration() { echo " Drive: $TARGET_DRIVE (BTRFS + LUKS2)" echo " Hostname: $HOSTNAME" echo " Username: $USERNAME" + echo " Keymap: $KEYMAP_LAYOUT${KEYMAP_VARIANT:+ ($KEYMAP_VARIANT)}" + echo " Locale: $LOCALE" echo " Timezone: $TIMEZONE" + echo " Form factor: $FORM_FACTOR" echo " Impermanence: $ENABLE_IMPERMANENCE" echo " Nomarchy rev: ${NOMARCHY_REV:-main (unpinned)}" echo "" @@ -821,12 +931,29 @@ EOF # system.nix — curated system-level options. Uncomment what you want and # 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 { pkgs, ... }: { networking.hostName = "$HOSTNAME"; 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 # 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 # `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, ... }: { + # 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). # # Nomarchy already ships a minimal desktop (firefox, thunar, mpv, imv, mako, # hyprlock, swww, wl-clipboard, grim, slurp, rofi-wayland, etc.). The list # below is a menu of extras — uncomment what you want and run - # `nomarchy-env-update`. + # \`nomarchy-env-update\`. home.packages = with pkgs; [ # --- Enabled by default --- btop # Resource monitor (TUI) @@ -989,8 +1123,8 @@ EOF # --- Optional Nomarchy app modules --- # opencode AI coding CLI integration (deploys ~/.config/opencode/opencode.json). - # The `opencode` package itself is not installed automatically — add it to - # `home.packages` above if you want it on PATH. + # The \`opencode\` package itself is not installed automatically — add it to + # \`home.packages\` above if you want it on PATH. # nomarchy.apps.opencode.enable = true; # Extra Home Manager modules go here (program configs, services, etc.). @@ -1041,8 +1175,10 @@ main() { select_disk get_luks_passphrase configure_user + select_keymap_locale select_timezone select_hardware + confirm_form_factor configure_impermanence review_configuration execute_installation