commit 19fd794b6b97f2a3fe04d46c22eea9447ce3c373 Author: 28allday Date: Sun Jun 21 08:51:50 2026 +0100 NoSignal — fully-offline Arch → Hyprland desktop installer Single-script builder (nosignal.sh) that turns a stock Arch Linux ISO into a fully-offline installer for a themed Hyprland + caelestia (Quickshell) desktop: matching SDDM greeter, Btrfs/Limine bootable snapshots, chwd-style GPU detection, and a curated "os updates" layer (keybind cheatsheet, settings panels, system polish, on-box management skill). See README.md. Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e04966b --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Build artifacts +work/ +*.iso +build*.log + +# Editor / OS cruft +*.swp +.DS_Store + +# Offline payload cache (multi-GB: built AUR pkgs + repo mirror + chroot) +offline/ + +# VM test rig (QEMU certification harness) — maintainer-only dev tooling, +# kept LOCAL (scripts + disks/firmware/screenshots/logs all stay off the repo). +vm/ + +# squashfs debug extracts +sq/ +*.sfs + +# Integration inbox from live test boxes — content is fully folded into +# "os updates/" + nosignal.sh; kept locally for reference only +# (-done = archived rounds; hypros-* = pre-rename rounds 2+3) +nosignal-handoff/ +nosignal-handoff-done/ +hypros-handoff/ +hypros-handoff-done/ + +# Dev-internal docs — kept LOCAL only, never in the public repo: +# the dev status log, hardware-test briefs, QA/builder handoff, and the +# internal phase/design docs (infra hostnames, dev-machine specs, process). +NOTES.md +ONBOX-BRIEF-*.md +dev-docs/ +docs/PHASE2-DESIGN.md +docs/PHASE3-OFFLINE.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5acd172 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Gavin Nugent + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3fe9147 --- /dev/null +++ b/README.md @@ -0,0 +1,202 @@ +# NoSignal OS + +A single-script builder that turns a stock Arch Linux ISO into a **fully +offline installer** for a ready-to-use **Hyprland desktop**: Arch + +[Hyprland](https://hyprland.org) + the +[caelestia](https://github.com/caelestia-dots/caelestia) dotfiles/shell, with +a themed [SDDM](https://github.com/sddm/sddm) login screen that matches the +desktop. + +Everything needed for the install is bundled **on the ISO** — a complete +pacman repo (every package incl. all GPU driver variants, with the caelestia +stack pre-built from the AUR at ISO-build time), the pinned dotfiles, the +theme. **No internet is needed to install or on first boot**: the machine +boots straight into the themed login screen and a fully configured desktop. + +> NoSignal is the *ISO builder*. It bundles and installs the upstream caelestia +> dotfiles; it is not affiliated with that project. + +> ⚠️ **Experimental — and built to be modified.** NoSignal OS is young and +> opinionated; expect rough edges. It is designed to be reshaped by *you*: every +> NoSignal tweak sits in a config-override layer on top of stock caelestia +> (delete a file to revert one), and the installed system ships an **on-box AI +> guide** so you can customise and fix it with +> [Claude Code](https://claude.com/claude-code) — see +> **[Customising the desktop](#customising-the-desktop-on-box-ai-guide)** below. + +## What you get on the installed system + +- **Hyprland + caelestia shell** (Quickshell), restyled with a restrained + flat look (no blur/glassmorphism) and the OS's own Moebius-style wallpaper + set (dynamic Material palette generated from the wallpaper). +- **Themed SDDM login** that mirrors the desktop palette/wallpaper/font + (`sudo sddm-theme-sync` re-syncs it after you change wallpaper). +- **Omarchy-style keybindings** — `Super+K` opens a searchable cheatsheet of + every bind. Universal `Super+C`/`Super+V` copy/paste, `Print` = region + screenshot, `Super+Space` launcher, `Super+D` dashboard/calendar, + `Super+Grave` workspace overview. +- **Software story**: `yay` (AUR), the **Shelly** GUI store on `Super+I` + (repos + AUR + Flathub), flatpak preconfigured. +- **Btrfs + bootable snapshots**: every package transaction snapshots + (snapper + snap-pac), and each snapshot appears as a bootable entry in the + **Limine** menu (10-second timeout) — roll back a bad update from the boot + screen. +- **`nosignal-update`**: one command for snapshot → package upgrade → layer + migrations; a Settings → Updates page shows pending counts. +- **Gaming (opt-in)**: the `[multilib]` repo is enabled and Omarchy-installer + shims are included, so + [DeckShift](https://github.com/28allday/DeckShift)-style Steam gaming + sessions can be added post-install — including a login fix that keeps a + password on cold boots. Not active unless you set it up. +- Bluetooth, printing (CUPS + network discovery), power-profiles, zram swap, + ufw on, locale/timezone/keymap of your choice. + +## Customising the desktop (on-box AI guide) + +NoSignal OS is meant to be tinkered with, so nothing is locked down. Every +NoSignal change lives in a **config-override layer** at `~/.config/caelestia/` +on top of stock caelestia — edit a file to change behaviour, delete it to fall +back to the default. Updates and rollbacks are safe: `nosignal-update` snapshots +before every upgrade, and each snapshot is a bootable entry in the Limine menu. + +To make changes easy — even if you don't know the internals — the installed +system ships an **on-box AI skill for [Claude Code](https://claude.com/claude-code)**. +It explains exactly what's safe to edit, how to change keybinds / monitors / +wallpaper / theme, how updates and snapshots work, how to install software, and +how to recover the desktop if the shell ever fails to start. + +- **Just ask.** Run `claude` on a NoSignal machine and it **auto-discovers** the + `nosignal` skill — say *"customise my NoSignal desktop"* or *"the bar didn't + load, fix it"* and it reads the guide before touching anything. +- **Point an assistant at the files directly** if you prefer: + - `~/.claude/skills/nosignal/SKILL.md` — the Claude skill (guardrails + tasks) + - `~/.local/share/nosignal/ONBOX-AI-NOTES.md` — the full system briefing + - `~/.local/share/nosignal/NoSignal-keybindings.md` — the keybinding reference + +The skill is strictly for managing **your installed system** — it carries no +build/release tooling, and it always reads the guardrails before editing config. + +## Build the ISO + +The builder runs on an **Arch Linux** (or Arch-based) host with an internet +connection — it uses `pacman`, `makepkg`, and `devtools` to assemble the offline +package set, so it can't run on a non-Arch distro. + +**1. Install the build dependencies:** + +```bash +sudo pacman -S --needed git libisoburn squashfs-tools coreutils devtools pacman-contrib +``` + +**2. Clone this repository and enter it:** + +```bash +git clone https://github.com/28allday/NoSignal-OS.git +cd NoSignal-OS +``` + +**3. Download a stock Arch Linux ISO into this folder.** The filename must start +with `archlinux-`. Grab the latest from any mirror, e.g.: + +```bash +curl -LO https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso +``` + +(or pick a mirror from .) + +**4. Run the builder:** + +```bash +./nosignal.sh +``` + +It downloads the full package closure and builds the AUR pieces in a clean +chroot — allow roughly **15–40 minutes** on the first run (you'll be asked for +`sudo`). Everything is cached in `./offline/`, so later rebuilds take a couple of +minutes. + +**5. Done.** `nosignal-arch-YYYYMMDD.iso` (~4 GB) appears in the folder — +Ventoy-compatible and `dd`-bootable. Write it to a USB stick and install (see +**Install** below). + +## Install + +**1. Write the ISO to a USB stick.** Replace `/dev/sdX` with your USB device +(check it with `lsblk` first — this erases the whole stick): + +```bash +sudo dd if=nosignal-arch-YYYYMMDD.iso of=/dev/sdX bs=4M status=progress conv=fsync +``` + +(or just drop the `.iso` onto a [Ventoy](https://www.ventoy.net) USB.) + +**2. Boot the USB** on the target machine — **UEFI only** (no BIOS/MBR). + +**3. Follow the installer.** It is **fully offline** (no network questions) and +asks for: hostname, user, password, your region (timezone / locale / keyboard), +and the target disk. It then **wipes that disk** — 1G EFI + btrfs with +`@`/`@home`/`@snapshots`/`@log` subvolumes and the Limine bootloader — and runs +a clean, centred progress screen. **It erases the chosen disk — back up first.** + +**4. Reboot** into the themed SDDM login and your desktop. Connect Wi-Fi from the +desktop's network panel; once you're online, run `sudo pacman -Syu` one time to +sync the package databases. + +## GPU detection + +The installer auto-detects the GPU(s) and installs the right drivers, +borrowing CachyOS [`chwd`](https://github.com/CachyOS/chwd)'s **method** (PCI +display-class scan → vendor-id → driver set) but implemented natively, so the +ISO stays vanilla Arch (no CachyOS repos / lib32 / cachyos-kernel deps). +Multiple GPUs union their package sets (e.g. an Intel/AMD iGPU + an NVIDIA +dGPU). All variants are bundled in the offline repo; only the matching subset +is installed. + +| Detected | Packages | +|----------|----------| +| NVIDIA (`10de`) | `nvidia-open-dkms nvidia-utils egl-wayland libva-nvidia-driver nvidia-settings linux-headers` + DRM/KMS setup | +| AMD (`1002`) | `mesa vulkan-radeon` | +| Intel (`8086`) | `mesa vulkan-intel intel-media-driver` | +| VM guest (`1af4`/`1b36`/`1234`/`15ad`/`80ee`) | `qemu-guest-agent spice-vdagent` | +| Unknown | `vulkan-swrast` (software fallback) | + +For NVIDIA it also does the Wayland-critical bits (the same ones chwd does): +early-loads `nvidia nvidia_modeset nvidia_uvm nvidia_drm` in the initramfs, +drops the `kms` hook so nouveau doesn't grab the card, sets +`nvidia_drm.modeset=1`, and writes the EGL/VA env to `/etc/environment`. + +## Notes / current limitations + +- **UEFI only.** No BIOS/MBR boot. +- **No disk encryption** yet. +- **NVIDIA uses the open kernel module** (`nvidia-open-dkms`), which is for + Turing and newer (RTX 20-series+). Older Maxwell/Pascal cards would need + the proprietary/legacy branch — not yet selected by generation. +- The bundled packages are a snapshot from build day — run `sudo pacman -Syu` + once online. + +## Credits + +NoSignal OS is a thin builder around excellent upstream work — it bundles, +themes, and installs these projects rather than replacing them. Please support +the originals. Each remains under its own licence. + +- **[Arch Linux](https://archlinux.org)** — the base system and live ISO. +- **[Hyprland](https://hyprland.org)** — the Wayland compositor. +- **[caelestia](https://github.com/caelestia-dots/caelestia)** — the desktop + dotfiles and shell that define the look and most of the UX. The installed + shell is a light soft-fork; the CLI / theming engine is upstream, unchanged. +- **[Quickshell](https://quickshell.outfoxxed.me)** — the runtime the shell runs on. +- **[SDDM](https://github.com/sddm/sddm)** — the login manager (themed to match). +- **[Limine](https://github.com/limine-bootloader/limine)**, with **snapper** and + **snap-pac** — bootloader and Btrfs snapshot tooling (bootable snapshots). +- **[Omarchy](https://omarchy.org)** — keybinding layout and UX inspiration, and + its prebuilt `[omarchy]` package repo for the Limine snapshot tools. +- **[CachyOS `chwd`](https://github.com/CachyOS/chwd)** — the GPU-detection + *method* (PCI scan → vendor-id → driver set), reimplemented natively here. +- Fonts: **JetBrains Mono Nerd Font** and **Material Symbols**. + +## License + +MIT — see [LICENSE](LICENSE). The wallpaper art set is the project author's +own original work and ships with the ISO under the same licence. diff --git a/assets/fastfetch-config.jsonc b/assets/fastfetch-config.jsonc new file mode 100644 index 0000000..c9dd090 --- /dev/null +++ b/assets/fastfetch-config.jsonc @@ -0,0 +1,138 @@ +{ + "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", + "logo": { + "type": "file", + "source": "~/.config/fastfetch/logo.txt", + "color": { "1": "green" }, + "padding": { + "top": 2, + "right": 6, + "left": 2 + } + }, + "modules": [ + "break", + { + "type": "custom", + "format": "\u001b[90m┌──────────────────────Hardware──────────────────────┐" + }, + { + "type": "host", + "key": " PC", + "keyColor": "green" + }, + { + "type": "cpu", + "key": "│ ├", + "showPeCoreCount": true, + "keyColor": "green" + }, + { + "type": "gpu", + "key": "│ ├", + "detectionMethod": "pci", + "keyColor": "green" + }, + { + "type": "display", + "key": "│ ├󱄄", + "keyColor": "green" + }, + { + "type": "disk", + "key": "│ ├󰋊", + "keyColor": "green" + }, + { + "type": "memory", + "key": "│ ├", + "keyColor": "green" + }, + { + "type": "swap", + "key": "└ └󰓡 ", + "keyColor": "green" + }, + { + "type": "custom", + "format": "\u001b[90m└────────────────────────────────────────────────────┘" + }, + "break", + { + "type": "custom", + "format": "\u001b[90m┌──────────────────────Software──────────────────────┐" + }, + { + "type": "command", + "key": " OS", + "keyColor": "blue", + "text": "echo \"NoSignal\"" + }, + { + "type": "kernel", + "key": "│ ├", + "keyColor": "blue" + }, + { + "type": "wm", + "key": "│ ├", + "keyColor": "blue" + }, + { + "type": "de", + "key": " DE", + "keyColor": "blue" + }, + { + "type": "terminal", + "key": "│ ├", + "keyColor": "blue" + }, + { + "type": "packages", + "key": "│ ├󰏖", + "keyColor": "blue" + }, + { + "type": "wmtheme", + "key": "│ ├󰉼", + "keyColor": "blue" + }, + { + "type": "command", + "key": "│ ├󰸌", + "keyColor": "blue", + "text": "theme=$(caelestia scheme get 2>/dev/null | awk -F': *' '/^Name:/{print $2; exit}'); [ -n \"$theme\" ] || theme=dynamic; echo -e \"$theme \\e[38m●\\e[37m●\\e[36m●\\e[35m●\\e[34m●\\e[33m●\\e[32m●\\e[31m●\"" + }, + { + "type": "terminalfont", + "key": "└ └", + "keyColor": "blue" + }, + { + "type": "custom", + "format": "\u001b[90m└────────────────────────────────────────────────────┘" + }, + "break", + { + "type": "custom", + "format": "\u001b[90m┌────────────────────Age / Uptime────────────────────┐" + }, + { + "type": "command", + "key": "󱦟 OS Age", + "keyColor": "magenta", + "text": "birth_install=$(stat -c %W /); current=$(date +%s); time_progression=$((current - birth_install)); days_difference=$((time_progression / 86400)); echo $days_difference days" + }, + { + "type": "uptime", + "key": "󱫐 Uptime", + "keyColor": "magenta" + }, + { + "type": "custom", + "format": "\u001b[90m└────────────────────────────────────────────────────┘" + }, + "break" + ] +} diff --git a/assets/fastfetch-logo.txt b/assets/fastfetch-logo.txt new file mode 100644 index 0000000..fa69744 --- /dev/null +++ b/assets/fastfetch-logo.txt @@ -0,0 +1,26 @@ + ⢀⣀⣤⣴⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣦⣤⣀⡀ + ⢀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⡀ + ⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⡀ + ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀ + ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀ + ⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄ + ⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆ + ⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆ + ⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⠿⡿⠿⠿⠿⣿⣿⣿⠿⠿⠿⠿⢿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⡆ +⢀⣿⣿⣿⣿⣶⡶⠸⣿⣿⣿⣿⠛⠛⠃⠛⠛⠛⢻⣿⡟⢐⣓⣒⣒⣒⣛⣿⣿⠰⠶⠶⠶ ⣿⣿⣛⡋⢈⣉⡭⠿⢿⣿⡇⢸⣿⣿⣿⣿⣿⡀ +⢸⣿⣿⣿⣿⣿⢃⡄⢻⣿⣿⣿⣿⣿⠃⣿⣿⡏⢸⣏⡄⢸⣗⣒⣒⣒⣺⣯⣉⡉⣉⣉⣉⣉⣉⣿⡿⢁⣿⣷⡒⣶⣾⣿⡇⢸⣿⣿⣿⣿⣿⡇ +⣿⣿⣿⣿⠿⢁⣾⣿⣄⠻⢿⣿⡿⢃⣼⣿⣿⡇⣸⣿⡇⢸⠁⣤⣤⡤⢸⣿⣿⣤⣶⣶⣶⢀⣿⣿⣥⣾⠩⣤⠄⣙⢻⣿⣇⠸⠿⠿⠟⣩⣿⣿ +⣿⣿⣿⣷⣶⣿⣿⣿⣿⣷⣶⣷⣶⣿⣿⣿⣤⣴⣿⣿⣧⣾⣦⣶⣶⣶⣼⣿⣿⣿⣿⣤⣤⣾⣿⣿⣿⣿⣶⣶⣾⣿⣿⣿⣿⣷⣶⣶⣿⣿⣿⣿ +⣿⣿⣿⣿⣿⣿⣿ ⠙⢿ ⢸⡟⢁⣠⡀⠙⣿⣿⣿⡏⠠⣤⣿⡇⢸⡟⠁⣠⣄⣙⣿ ⠈⢻⡇⢸⣿⡏ ⠘⣿⡇ ⣿⣿⣿⣿⣿⣿⣿⣿ +⣿⣿⣿⣿⣿⣿⣿ ⣦⡀ ⢸⡄⠘⠿⠟ ⣿⣿⣿⡷⠦ ⢹⡇⢸⣇ ⠿⠄ ⣿ ⢰⡄⠁⢸⡟ ⠘ ⢹⡇ ⠿⢿⣿⣿⣿⣿⣿⣿ +⢸⣿⣿⣿⣿⣿⣿⣶⣿⣿⣶⣾⣿⣶⣦⣶⣾⣿⣿⣿⣶⣶⣶⣿⣷⣾⣿⣷⣶⣶⣾⣿⣶⣾⣿⣶⣾⣷⣾⣿⣷⣶⣷⣶⣶⣾⣿⣿⣿⣿⣿⡇ +⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁ + ⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇ + ⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏ + ⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏ + ⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋ + ⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁ + ⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁ + ⠈⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠁ + ⠈⠙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠋⠁ + ⠈⠉⠛⠻⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠟⠛⠉⠁ diff --git a/assets/plymouth/nosignal/logo.png b/assets/plymouth/nosignal/logo.png new file mode 100644 index 0000000..33bf67d Binary files /dev/null and b/assets/plymouth/nosignal/logo.png differ diff --git a/assets/plymouth/nosignal/nosignal.plymouth b/assets/plymouth/nosignal/nosignal.plymouth new file mode 100644 index 0000000..bc99d51 --- /dev/null +++ b/assets/plymouth/nosignal/nosignal.plymouth @@ -0,0 +1,8 @@ +[Plymouth Theme] +Name=NoSignal +Description=NoSignal boot splash — NO SIGNAL logo on black +ModuleName=script + +[script] +ImageDir=/usr/share/plymouth/themes/nosignal +ScriptFile=/usr/share/plymouth/themes/nosignal/nosignal.script diff --git a/assets/plymouth/nosignal/nosignal.script b/assets/plymouth/nosignal/nosignal.script new file mode 100644 index 0000000..c7da5c1 --- /dev/null +++ b/assets/plymouth/nosignal/nosignal.script @@ -0,0 +1,43 @@ +# NoSignal Plymouth splash — the "NO SIGNAL / 入力信号なし" logo centred on black. +# Shows between Limine and the SDDM greeter (and on shutdown/reboot). The logo's +# black disc blends into the background, so only the green text floats on black. + +# Solid black background. +Window.SetBackgroundTopColor(0, 0, 0); +Window.SetBackgroundBottomColor(0, 0, 0); + +screen_width = Window.GetWidth(); +screen_height = Window.GetHeight(); + +# Scale the logo to ~67.5% of the shorter screen edge (0.45 enlarged by half), +# keeping aspect ratio, so it is a sensible size on both 1080p and 4K. +short_edge = screen_height; +if (screen_width < screen_height) + short_edge = screen_width; +target = short_edge * 0.675; + +orig = Image("logo.png"); +scale = target / orig.GetHeight(); +logo.image = orig.Scale(orig.GetWidth() * scale, orig.GetHeight() * scale); +logo.sprite = Sprite(logo.image); +logo.sprite.SetX(screen_width / 2 - logo.image.GetWidth() / 2); +logo.sprite.SetY(screen_height / 2 - logo.image.GetHeight() / 2); + +# Static splash — full opacity, no animation. +logo.sprite.SetOpacity(1); + +# Subtle status line (fsck / "press a key" style messages) below the logo. +message.sprite = Sprite(); +message.sprite.SetX(20); +message.sprite.SetY(screen_height - 36); +fun message_callback(text) { + message.image = Image.Text(text, 0.66, 0.84, 0.45); # muted NoSignal green + message.sprite.SetImage(message.image); +} +Plymouth.SetMessageFunction(message_callback); + +# On quit (handing off to the greeter), snap the logo to full opacity. +fun quit_callback() { + logo.sprite.SetOpacity(1); +} +Plymouth.SetQuitFunction(quit_callback); diff --git a/assets/scheme-shadotheme.json b/assets/scheme-shadotheme.json new file mode 100644 index 0000000..8883e6f --- /dev/null +++ b/assets/scheme-shadotheme.json @@ -0,0 +1 @@ +{"name": "shadotheme", "flavour": "default", "mode": "dark", "variant": "tonalspot", "colours": {"primary_paletteKeyColor": "6f72ac", "secondary_paletteKeyColor": "75758e", "tertiary_paletteKeyColor": "9d648f", "neutral_paletteKeyColor": "78767b", "neutral_variant_paletteKeyColor": "777680", "background": "131317", "onBackground": "e5e1e7", "surface": "131317", "surfaceDim": "131317", "surfaceBright": "39393d", "surfaceContainerLowest": "0e0e11", "surfaceContainerLow": "1b1b1f", "surfaceContainer": "1f1f23", "surfaceContainerHigh": "2a292e", "surfaceContainerHighest": "353438", "onSurface": "e5e1e7", "surfaceVariant": "46464f", "onSurfaceVariant": "c7c5d1", "inverseSurface": "e5e1e7", "inverseOnSurface": "303034", "outline": "918f9a", "outlineVariant": "46464f", "shadow": "000000", "scrim": "000000", "surfaceTint": "bfc1ff", "primary": "bfc1ff", "onPrimary": "282b60", "primaryContainer": "6f72ac", "onPrimaryContainer": "000028", "inversePrimary": "565992", "secondary": "c5c4e0", "onSecondary": "2e2f44", "secondaryContainer": "44455c", "onSecondaryContainer": "b3b3ce", "tertiary": "f4b2e2", "onTertiary": "4e1e45", "tertiaryContainer": "ba7eaa", "onTertiaryContainer": "000000", "error": "ffb4ab", "onError": "690005", "errorContainer": "93000a", "onErrorContainer": "ffdad6", "primaryFixed": "e0e0ff", "primaryFixedDim": "bfc1ff", "onPrimaryFixed": "12144a", "onPrimaryFixedVariant": "3e4278", "secondaryFixed": "e1e0fd", "secondaryFixedDim": "c5c4e0", "onSecondaryFixed": "191a2e", "onSecondaryFixedVariant": "44455c", "tertiaryFixed": "ffd7f1", "tertiaryFixedDim": "f4b2e2", "onTertiaryFixed": "35082f", "onTertiaryFixedVariant": "67355d", "term0": "353434", "term1": "a875ff", "term2": "44def5", "term3": "ffdcf2", "term4": "97aad7", "term5": "b29feb", "term6": "9dceff", "term7": "e8d3de", "term8": "ac9fa9", "term9": "bd95ff", "term10": "89ecff", "term11": "fff0f6", "term12": "b4c1dc", "term13": "c8b5f5", "term14": "bae0ff", "term15": "ffffff", "rosewater": "f7eff9", "flamingo": "e8def3", "pink": "e1d8ff", "mauve": "bdb9ff", "red": "bfa6fe", "maroon": "c7b6ed", "peach": "e0c2f9", "yellow": "ffecf3", "green": "c8e3ff", "teal": "d2e0ff", "sky": "cedaff", "sapphire": "b5c5ff", "blue": "aeb8ff", "lavender": "c6c8ff", "klink": "7083d2", "klinkSelection": "6f83d2", "kvisited": "7e73db", "kvisitedSelection": "7d73db", "knegative": "9d69ff", "knegativeSelection": "9b6aff", "kneutral": "c794ff", "kneutralSelection": "c794ff", "kpositive": "60adff", "kpositiveSelection": "60adff", "text": "e5e1e7", "subtext1": "c7c5d1", "subtext0": "918f9a", "overlay2": "7e7c86", "overlay1": "6b6972", "overlay0": "595860", "surface2": "48474e", "surface1": "37373d", "surface0": "25252a", "base": "131317", "mantle": "131317", "crust": "121216", "success": "B5CCBA", "onSuccess": "213528", "successContainer": "374B3E", "onSuccessContainer": "D1E9D6"}} \ No newline at end of file diff --git a/assets/wallpapers/bridge-city.png b/assets/wallpapers/bridge-city.png new file mode 100644 index 0000000..17532ea Binary files /dev/null and b/assets/wallpapers/bridge-city.png differ diff --git a/assets/wallpapers/cloud-harbour.png b/assets/wallpapers/cloud-harbour.png new file mode 100644 index 0000000..a87b053 Binary files /dev/null and b/assets/wallpapers/cloud-harbour.png differ diff --git a/assets/wallpapers/desert-monolith.png b/assets/wallpapers/desert-monolith.png new file mode 100644 index 0000000..ae9ff0e Binary files /dev/null and b/assets/wallpapers/desert-monolith.png differ diff --git a/assets/wallpapers/desert-outpost.png b/assets/wallpapers/desert-outpost.png new file mode 100644 index 0000000..459e042 Binary files /dev/null and b/assets/wallpapers/desert-outpost.png differ diff --git a/assets/wallpapers/foam-sea.png b/assets/wallpapers/foam-sea.png new file mode 100644 index 0000000..b35d72f Binary files /dev/null and b/assets/wallpapers/foam-sea.png differ diff --git a/assets/wallpapers/ice-fleet.png b/assets/wallpapers/ice-fleet.png new file mode 100644 index 0000000..0d024cb Binary files /dev/null and b/assets/wallpapers/ice-fleet.png differ diff --git a/assets/wallpapers/sail-tracker.png b/assets/wallpapers/sail-tracker.png new file mode 100644 index 0000000..57e1010 Binary files /dev/null and b/assets/wallpapers/sail-tracker.png differ diff --git a/assets/wallpapers/sky-towers.png b/assets/wallpapers/sky-towers.png new file mode 100644 index 0000000..6c191b2 Binary files /dev/null and b/assets/wallpapers/sky-towers.png differ diff --git a/assets/wallpapers/tidepools.png b/assets/wallpapers/tidepools.png new file mode 100644 index 0000000..4053785 Binary files /dev/null and b/assets/wallpapers/tidepools.png differ diff --git a/assets/wallpapers/wanderer.png b/assets/wallpapers/wanderer.png new file mode 100644 index 0000000..88fb91a Binary files /dev/null and b/assets/wallpapers/wanderer.png differ diff --git a/assets/wallpapers/wave-watchers.png b/assets/wallpapers/wave-watchers.png new file mode 100644 index 0000000..d3f2ae3 Binary files /dev/null and b/assets/wallpapers/wave-watchers.png differ diff --git a/nosignal.sh b/nosignal.sh new file mode 100755 index 0000000..7e4683e --- /dev/null +++ b/nosignal.sh @@ -0,0 +1,2366 @@ +#!/usr/bin/env bash +# +# nosignal.sh — one script: turn a stock Arch ISO into a graphical Arch desktop +# installer ISO. The installed system boots into SDDM (themed to match the +# desktop — layer changes 16+19; was greetd+tuigreet) and runs Hyprland, +# themed with the caelestia dotfiles +# (https://github.com/caelestia-dots/caelestia). +# +# Architecture (Phase 3 — FULLY OFFLINE): the ISO bundles EVERYTHING needed +# for the install. At build time, this script: +# * builds the caelestia AUR packages (shell/CLI/meta + quickshell-git and +# friends) in a devtools clean chroot on the build host, +# * downloads the complete pacstrap dependency tree (including every GPU +# driver variant and the prebuilt Limine snapshot tools from the omarchy +# repo) into a local pacman repo, +# * vendors the caelestia dotfiles + quickshell-overview as pinned tarballs, +# and places all of it on the ISO9660 (outside the squashfs, visible to the +# live environment at /run/archiso/bootmnt/nosignal). The installer pacstraps +# entirely from that file:// repo and lays down the whole desktop — dotfiles, +# theming, workspace overview, Btrfs+Limine snapshots — at INSTALL TIME in the +# chroot. No network is needed at install or on first boot: the machine boots +# straight into the themed SDDM greeter and a fully themed desktop. (The old +# first-boot build stage — paru bootstrap, nmtui gate, tty1 autologin — is gone.) +# +# Network IS needed on the BUILD host (AUR builds + repo download), and the +# Wi-Fi prompt remains in the installer as an OPTIONAL step purely so the +# credentials can be persisted into the installed system. +# +# NOTE: this is the *ISO builder*. It bundles and installs the upstream +# caelestia dotfiles; it is not affiliated with that project. +# +# Usage: +# 1. Drop a stock Arch ISO (https://archlinux.org/download/, file starts +# with 'archlinux-') into the same folder as this script. +# 2. ./nosignal.sh +# 3. Output: nosignal-arch-YYYYMMDD.iso (Ventoy-compatible, dd-bootable). +# +# The offline payload is cached in ./offline/ — subsequent builds reuse the +# built AUR packages and previously downloaded repo packages. Delete that +# folder (or individual .built-* stamps in offline/aur/) to force a rebuild. +# +# Dependencies (build host): xorriso, squashfs-tools, git, sha512sum, sudo, +# devtools (mkarchroot/makechrootpkg), pacman-contrib (paccache). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORK="$SCRIPT_DIR/work" +OFFLINE="$SCRIPT_DIR/offline" +INVOKING_USER="${SUDO_USER:-$(id -un)}" +INVOKING_GROUP="$(id -gn "$INVOKING_USER")" +OUT_ISO="$SCRIPT_DIR/nosignal-arch-$(date +%Y%m%d).iso" + +# ---------------------------------------------------------------- pins ------ +# The validated combination (VM-certified): caelestia dotfiles + the AUR +# release PKGBUILDs current at that commit, quickshell pinned to the commit +# the shell was validated against, and the overview sidecar pin. +CAELESTIA_DOTS_REPO="https://github.com/caelestia-dots/caelestia.git" +CAELESTIA_DOTS_COMMIT="757b7f26b8e637754ddd498671360dd6ba7e89ef" +OVERVIEW_REPO="https://github.com/Shanu-Kumawat/quickshell-overview.git" +OVERVIEW_COMMIT="74918ba66e0714017a6a5bfb9bf2affa66dabbc1" +QUICKSHELL_GIT_COMMIT="b66495f" # short hash is fine for makepkg #commit= +# NoSignal's fork of the caelestia shell — we BUILD this instead of the upstream +# caelestia-shell so the interface/layout is under our control and never gets +# overwritten by an upstream update (the package provides/conflicts caelestia-shell +# and is pinned here, so pacman/yay never replaces it). Updates are deliberate: +# advance the fork's `nosignal` branch, bump the pin, rebuild. The fork's +# packaging/PKGBUILD builds the shell from this exact commit. +NOSIGNAL_SHELL_REPO="https://github.com/28allday/nosignal-shell.git" +NOSIGNAL_SHELL_COMMIT="0ae859311310661555dae089c91f2c0bfaaec10b" +# OS theme wallpapers — the user's own Moebius-style generated art (11 pieces, +# 2944x1648, repo-local in assets/wallpapers/; replaced the third-party Last +# Horizon set 2026-06-11, which also clears the redistribution-rights concern). +# The desktop palette is generated FROM the default wallpaper by caelestia's +# dynamic Material scheme at install time. +WALLPAPERS_DIR="$SCRIPT_DIR/assets/wallpapers" +# foam-sea since change 18 (was wanderer.png). Build-time default only — no +# migration, so existing installs keep whatever the user picked. +DEFAULT_WALLPAPER="foam-sea.png" +# omarchy-send release binary (LocalSend-compatible LAN file transfer TUI). +OMARCHY_SEND_REPO="28allday/omarchy-send" +OMARCHY_SEND_VERSION="v0.1.11" +# NoSignal layer — the "os updates" round developed and live-tested on real +# hardware (keybind cheatsheet, bash default shell, nosignal-update, yay+Shelly +# store, Omarchy keybindings, omadots polish, hyprmoncfg, settings Updates +# page, system polish). The tree is vendored verbatim onto the ISO and baked +# into ~/.local/share/nosignal at install time. Dev-only docs (changelog, +# builder/test notes) are kept unshipped in the repo's dev-docs/ folder. +NOSIGNAL_LAYER_DIR="$SCRIPT_DIR/os updates" +# LazyVim starter (omadots' nvim setup, change 6) — pinned, .git stripped. +LAZYVIM_REPO="https://github.com/LazyVim/starter" +LAZYVIM_COMMIT="803bc181d7c0d6d5eeba9274d9be49b287294d99" +# Flathub remote definition (change 4). The .flatpakrepo file embeds the GPG +# key, so `flatpak remote-add` from it works OFFLINE in the install chroot. +FLATHUB_REPO_URL="https://dl.flathub.org/repo/flathub.flatpakrepo" + +# ----------------------------------------------------- package manifests ---- +# BASE_PKGS is the single source of truth for the non-GPU pacstrap set: it is +# written to the ISO as /nosignal/base-packages.list and read back by the +# installer (so the download closure and the installed set can never drift). +BASE_PKGS=( + base linux linux-firmware intel-ucode amd-ucode + networkmanager openssh sudo git curl wget vim less base-devel + limine efibootmgr zram-generator + btrfs-progs snapper snap-pac + pipewire pipewire-pulse pipewire-alsa pipewire-jack wireplumber + polkit hyprpolkitagent qt5-wayland qt6-wayland xdg-user-dirs xdg-utils + hyprland uwsm xdg-desktop-portal-hyprland xdg-desktop-portal-gtk + hyprpicker hyprlock hypridle hyprsunset wl-clipboard cliphist inotify-tools + grim slurp swappy brightnessctl playerctl pavucontrol + # Display manager: SDDM (layer change 16 — DeckShift's desktop<->gaming + # session switching rewrites SDDM config; greetd/tuigreet couldn't do this). + # xorg-server is for SDDM's X11 GREETER only (DisplayServer=x11, the + # reliable path) — the Hyprland session itself stays Wayland. + sddm xorg-server plymouth + # NB: fish must STAY even though bash is the default shell (NoSignal layer + # change 2) — caelestia-meta hard-depends on it. foot stays as a meta dep + # too (its menu entries are hidden by the layer's system-polish change). + foot fish fastfetch starship btop jq eza trash-cli + adw-gtk-theme papirus-icon-theme ttf-jetbrains-mono-nerd + ttf-material-symbols-variable ttf-cascadia-code-nerd + noto-fonts noto-fonts-emoji + kitty-terminfo foot-terminfo + ufw + caelestia-meta + # default terminal (foot stays as caelestia-themed fallback) + kitty + # everyday apps + chromium nautilus gvfs gvfs-mtp + mpv imv papers gnome-calculator gnome-text-editor + libreoffice-fresh + # default music player (layer change 24): CLIAmp, prebuilt in [omarchy] + # (no AUR build); pulls ffmpeg + yt-dlp into the closure + cliamp + # monitor layout/profiles TUI + hotplug daemon (writes the same + # ~/.config/hypr/monitors.conf the base config sources; superseded + # nwg-displays — keep a SINGLE writer of that file). AUR-built. + hyprmoncfg + # software installation story: yay (AUR helper, also used by + # nosignal-update) + Shelly GUI store (both AUR-built) + flatpak backend + flatpak archlinux-appstream-data + yay-bin shelly-bin + # NoSignal layer tooling: checkupdates (updates panel), desktop notify, + # lspci for the Additions page's GPU-detected OBS encoders (change 25 — + # present on the test boxes only as an incidental dep; make it explicit) + pacman-contrib libnotify pciutils + # omadots developer polish (starship/btop/eza already above) + tmux neovim fzf bat zoxide + # base default dev tooling (layer change 33): GitHub CLI (`gh`) + github-cli + # bluetooth + bluez bluez-utils blueman + # laptop: audio firmware + power profiles + sof-firmware power-profiles-daemon + # printing + network discovery + cups system-config-printer avahi nss-mdns + # GUI Wi-Fi fallback (non-CLI escape hatch beside the bar's Wi-Fi panel) + + # GL/Vulkan info tools so `prime-run glxinfo`/`vulkaninfo` work for verifying + # hybrid-GPU offload (hardware-test gap, 2026-06-20) + nm-connection-editor mesa-utils vulkan-tools +) + +# Every GPU driver variant goes into the offline repo; the installer detects +# the hardware and installs only the matching subset. KEEP IN SYNC with the +# GPU detection block inside write_installer below. +GPU_ALL_PKGS=( + mesa vulkan-icd-loader + nvidia-open-dkms nvidia-utils egl-wayland libva-nvidia-driver + nvidia-settings linux-headers + nvidia-prime # prime-run offload (hybrid laptops) + vulkan-radeon + vulkan-intel intel-media-driver + qemu-guest-agent spice-vdagent + vulkan-swrast +) + +# Prebuilt binaries from the omarchy repo (skips their GraalVM AUR build). +LIMINE_TOOLS=(limine-snapper-sync limine-mkinitcpio-hook) + +# AUR packages built in the clean chroot, in dependency order (each build can +# resolve the previously built ones through the local bootstrap repo). +# caelestia-meta is NOT here — it builds from the dotfiles clone on the host. +AUR_BUILD_ORDER=( + app2unit libcava python-materialyoucolor ttf-rubik-vf qtengine + quickshell-git caelestia-cli nosignal-shell + # NoSignal layer round — independent of the caelestia chain (yay-bin and + # shelly-bin repackage release binaries; hyprmoncfg ships a Go release) + yay-bin shelly-bin hyprmoncfg +) + +# Clean up the work dir on any exit (success, error, or Ctrl-C). 2>/dev/null +# is in case $WORK never got created (e.g. we exited before mkdir). The +# offline payload cache in $OFFLINE is deliberately NOT cleaned. +trap 'sudo rm -rf "$WORK" 2>/dev/null || true' EXIT + +# =========================================================================== +# write_installer — emit the live-ISO installer to the given path. +# This function holds the script that runs on the *target* machine inside +# the live ISO (auto-launched on tty1). It is intentionally embedded here so +# this file is the only artifact needed to rebuild the installer ISO. +# =========================================================================== +write_installer() { + cat > "$1" <<'__INSTALLER_PAYLOAD__' +#!/usr/bin/env bash +set -euo pipefail + +MARKER="/tmp/.installer-ran" +if [ -f "$MARKER" ]; then + echo + echo " Installer already ran this session." + echo " To retry: rm $MARKER && bash $0" + echo + exec /bin/bash +fi +touch "$MARKER" + +# Mirror stdout/stderr to /tmp/installer.log. If the script aborts, the user +# can switch to tty2, log in as root, and `cat /tmp/installer.log` to see the +# last "==> ..." marker reached and any error printed by the failing command. +exec > >(tee /tmp/installer.log) 2>&1 + +if [ ! -d /sys/firmware/efi ]; then + echo "ERROR: not booted in UEFI mode. UEFI-only installer." >&2 + exec /bin/bash +fi + +clear + +# --- styled UI helpers (pure bash, no extra packages). They draw to /dev/tty +# directly so the arrow-key redraws never spam the tee'd installer.log. The UI +# is centre-aligned to the console width. -------------------------------------- +NSI_G=$'\033[38;5;150m' # NoSignal green +NSI_GB=$'\033[1;38;5;150m' # bold green +NSI_DIM=$'\033[0;90m' # dim/grey +NSI_B=$'\033[1m' +NSI_R=$'\033[0m' + +# Console width (from the real console — stdout is tee'd to a pipe). Default 80. +nsi_cols() { + local c; c=$(stty size /dev/null | awk '{print $2}') + case "$c" in ''|*[!0-9]*) c=80 ;; esac + [ "$c" -ge 20 ] 2>/dev/null || c=80 + printf '%s' "$c" +} +# Left-pad string for centring a block of visible width $1. +nsi_pad() { + local cols p; cols=$(nsi_cols); p=$(( (cols - $1) / 2 )); [ "$p" -lt 0 ] && p=0 + printf '%*s' "$p" '' +} +# Centred line of PLAIN text. $2=colour code (optional), $3=reset (optional). +cecho() { printf '%s%s%s%s\n' "$(nsi_pad "${#1}")" "${2:-}" "$1" "${3:-}"; } +# Centred read into VAR: cread VAR "prompt". csecret = silent (passwords). +cread() { printf '%s' "$(nsi_pad "${#2}")" >/dev/tty; read -rp "$2" "$1" /dev/tty; read -rsp "$2" "$1" sets MENU_CHOICE (0-based index) + MENU_VALUE. +# Navigate with ↑/↓ (or j/k), Enter selects. Centre-aligned as a block. +tui_menu() { + local title="$1"; shift + local -a opts=("$@") + local n=${#opts[@]} sel=0 first=1 i key rest o w + local lines=$(( n + 3 )) # title + blank + n options + hint + local hint='↑/↓ move · Enter select' + # Block width = widest of title, "❯ "+option, hint — then centre that block. + local maxw=${#title} + for o in "${opts[@]}"; do w=$(( ${#o} + 2 )); [ "$w" -gt "$maxw" ] && maxw=$w; done + [ "${#hint}" -gt "$maxw" ] && maxw=${#hint} + local pad; pad=$(nsi_pad "$maxw") + printf '\033[?25l' >/dev/tty + while true; do + if [ "$first" -eq 1 ]; then first=0; else printf '\033[%dA' "$lines" >/dev/tty; fi + { + printf '\r\033[2K%s%s%s%s\n\r\033[2K\n' "$pad" "$NSI_GB" "$title" "$NSI_R" + for i in "${!opts[@]}"; do + if [ "$i" -eq "$sel" ]; then + printf '\r\033[2K%s%s❯%s %s%s%s\n' "$pad" "$NSI_GB" "$NSI_R" "$NSI_B" "${opts[$i]}" "$NSI_R" + else + printf '\r\033[2K%s %s%s%s\n' "$pad" "$NSI_DIM" "${opts[$i]}" "$NSI_R" + fi + done + printf '\r\033[2K%s%s%s%s\n' "$pad" "$NSI_DIM" "$hint" "$NSI_R" + } >/dev/tty + IFS= read -rsn1 key /dev/tty + MENU_CHOICE=$sel + MENU_VALUE="${opts[$sel]}" +} + +# Install-progress UI. During the heavy install phase ALL command output is +# redirected to /tmp/installer.log (off-screen) and the user sees only a single +# CENTRED spinner + the current phase name on /dev/tty — calm and consistent +# with the centred menus, no scrolling formatting/build-hook spam. Full detail +# stays in the log (Ctrl+Alt+F2 -> cat /tmp/installer.log). +NSI_PHASE_FILE=/tmp/nsi-phase +NSI_SPIN_PID="" +# Centred line drawn straight to the real console (stdout is the log mid-install). +tcecho() { printf '%s%s%s%s\n' "$(nsi_pad "${#1}")" "${2:-}" "$1" "${3:-}" >/dev/tty; } +# Set the phase label the spinner shows. +nsi_phase() { printf '%s' "$1" > "$NSI_PHASE_FILE" 2>/dev/null || true; } +nsi_spin_start() { + printf '\033[?25l' >/dev/tty # hide cursor + ( + set +e + local frames='/-\|' i=0 msg vis pad + while true; do + msg=$(cat "$NSI_PHASE_FILE" 2>/dev/null); [ -n "$msg" ] || msg="Working" + vis=$(( ${#msg} + 5 )) # frame + 2 spaces + msg + ellipsis + pad=$(nsi_pad "$vis") + printf '\r\033[2K%s%s%s%s %s%s…%s' \ + "$pad" "$NSI_GB" "${frames:i%4:1}" "$NSI_R" "$NSI_B" "$msg" "$NSI_R" >/dev/tty + i=$((i+1)); sleep 0.15 + done + ) & + NSI_SPIN_PID=$! +} +nsi_spin_stop() { + [ -n "$NSI_SPIN_PID" ] && { kill "$NSI_SPIN_PID" 2>/dev/null || true; wait "$NSI_SPIN_PID" 2>/dev/null || true; NSI_SPIN_PID=""; } + printf '\r\033[2K\033[?25h' >/dev/tty # clear the spinner line, show cursor +} +# ERR trap during install: stop the spinner, restore on-screen output, and show +# which phase failed + the tail of the log (full detail on tty2). +nsi_fail() { + trap - ERR + nsi_spin_stop + exec >/dev/tty 2>&1 + echo + cecho "Install FAILED at: $(cat "$NSI_PHASE_FILE" 2>/dev/null)" "$NSI_GB" "$NSI_R" + cecho "See the log on tty2: Ctrl+Alt+F2 → cat /tmp/installer.log" "$NSI_DIM" "$NSI_R" + echo + tail -n 25 /tmp/installer.log 2>/dev/null +} + +# Banner — centred as a block. ASCII + box-drawing + Latin-1 middot only: the +# Linux console font has no CJK glyphs, so the kana rendered as blocks and (being +# double-width) pushed the right border out. The 入力信号なし wordmark lives on +# the Plymouth splash (a PNG) instead. Borders are 46 cells; interior is 44. +BPAD=$(nsi_pad 46) +printf '%s%s%s\n' "$BPAD" "$NSI_GB" "┌────────────────────────────────────────────┐" +printf '%s%s%s\n' "$BPAD" "$NSI_GB" "│ NoSignal · NO SIGNAL │" +printf '%s%s%s\n' "$BPAD" "$NSI_GB" "└────────────────────────────────────────────┘" +printf '%s' "$NSI_R" +cecho "Arch · Hyprland · Caelestia · SDDM — fully offline installer" "$NSI_DIM" "$NSI_R" +echo + +# ----------------------------------------------------- offline payload ------ +# Everything installs from the package repo bundled on the install media. +# archiso's copytoram defaults to AUTO and fires on every non-optical boot +# (USB stick + enough RAM): the initramfs copies airootfs.sfs to RAM, then +# UNMOUNTS and rmdir's /run/archiso/bootmnt — taking /nosignal with it. CD +# boots are exempt, which is why VM -cdrom certification never caught it. +# Remount the install media by the UUID archiso itself booted from. +NOSIGNAL_PAYLOAD=/run/archiso/bootmnt/nosignal +if [ ! -f "$NOSIGNAL_PAYLOAD/repo/nosignal.db" ]; then + cecho "Boot media unmounted by copytoram — remounting the offline repo..." "$NSI_DIM" "$NSI_R" + iso_uuid=$(sed -n 's/.*archisosearchuuid=\([^ ]*\).*/\1/p' /proc/cmdline) + iso_label=$(sed -n 's/.*archisolabel=\([^ ]*\).*/\1/p' /proc/cmdline) + iso_dev="" + for _ in $(seq 1 15); do + if [ -n "$iso_uuid" ] && [ -e "/dev/disk/by-uuid/$iso_uuid" ]; then + iso_dev="/dev/disk/by-uuid/$iso_uuid"; break + fi + if [ -n "$iso_label" ] && [ -e "/dev/disk/by-label/$iso_label" ]; then + iso_dev="/dev/disk/by-label/$iso_label"; break + fi + sleep 1 + done + if [ -n "$iso_dev" ]; then + mkdir -p /run/archiso/bootmnt + mount -o ro "$iso_dev" /run/archiso/bootmnt || true + fi +fi +if [ ! -f "$NOSIGNAL_PAYLOAD/repo/nosignal.db" ] || [ ! -f "$NOSIGNAL_PAYLOAD/base-packages.list" ]; then + echo "ERROR: offline package repo not found on the install media" >&2 + echo " (expected $NOSIGNAL_PAYLOAD/repo). Bad burn or wrong ISO?" >&2 + exec /bin/bash +fi +# Strip comments/blank lines so the list file can be annotated. +mapfile -t BASE_PKGS < <(grep -vE '^\s*(#|$)' "$NOSIGNAL_PAYLOAD/base-packages.list") +cecho "Offline repo found (${#BASE_PKGS[@]} base packages)." "$NSI_DIM" "$NSI_R" +echo + +# pacman config used for every install-time operation: ONLY the bundled repo. +# The installed system gets the normal online mirrors instead (written later). +# SigLevel Never: the bundled packages were verified when the ISO was BUILT +# (official sigs checked on download; AUR pkgs built locally) — the ISO is the +# trust boundary. Anything weaker fails: e.g. 'Optional TrustAll' still tries +# to VERIFY any signature embedded in the repo db and dies on the missing key. +cat > /tmp/nosignal-pacman.conf <<'OFFLINECONF' +[options] +Architecture = x86_64 +SigLevel = Never + +[nosignal] +Server = file:///run/archiso/bootmnt/nosignal/repo +OFFLINECONF + +# ---------------------------------------------------------------- network ---- +# The install is fully offline — no network is needed here or on first boot. +# Wired boxes DHCP automatically via NetworkManager; Wi-Fi users connect from +# the desktop's Wi-Fi panel (click-to-connect) or nm-connection-editor on first +# boot. So the installer asks for nothing here. + +# ---------------------------------------------------------------- prompts ---- +while true; do + cread HOSTNAME "Hostname [nosignal]: " + HOSTNAME="${HOSTNAME:-nosignal}" + [[ "$HOSTNAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}$ ]] && break + cecho "Invalid (RFC 1123: letters/digits/hyphen, no leading hyphen, max 63 chars)." +done + +while true; do + cread USERNAME "Username: " + [[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]{0,30}$ ]] && break + cecho "Invalid (lowercase, starts a-z or _, max 31 chars)." +done + +while true; do + csecret PW1 "Password for $USERNAME (also used for root): " + csecret PW2 "Confirm password: " + if [ -z "$PW1" ]; then cecho "Empty — try again."; continue; fi + if [ "$PW1" != "$PW2" ]; then cecho "Mismatch — try again."; continue; fi + break +done +USER_PW="$PW1" +echo + +# Localisation — pick a region from a curated list; it sets the timezone, locale, +# console keymap and Hyprland xkb layout together (no more three cryptic prompts). +# "Other" drops to the search-based prompts for anything not listed. The curated +# values are all standard tzdata/locale.gen/kbd names present on the ISO. +tui_menu "Where are you? (sets time zone, language & keyboard)" \ + "United Kingdom" \ + "Ireland" \ + "United States" \ + "Canada" \ + "Australia" \ + "Germany" \ + "France" \ + "Spain" \ + "Italy" \ + "Netherlands" \ + "Other (advanced — type & search)" +case "$MENU_VALUE" in + "United Kingdom") TIMEZONE=Europe/London; LOCALE=en_GB.UTF-8; KEYMAP=uk; XKB_LAYOUT=gb ;; + "Ireland") TIMEZONE=Europe/Dublin; LOCALE=en_IE.UTF-8; KEYMAP=uk; XKB_LAYOUT=gb ;; + "United States") TIMEZONE=America/New_York; LOCALE=en_US.UTF-8; KEYMAP=us; XKB_LAYOUT=us ;; + "Canada") TIMEZONE=America/Toronto; LOCALE=en_CA.UTF-8; KEYMAP=us; XKB_LAYOUT=us ;; + "Australia") TIMEZONE=Australia/Sydney; LOCALE=en_AU.UTF-8; KEYMAP=us; XKB_LAYOUT=us ;; + "Germany") TIMEZONE=Europe/Berlin; LOCALE=de_DE.UTF-8; KEYMAP=de-latin1; XKB_LAYOUT=de ;; + "France") TIMEZONE=Europe/Paris; LOCALE=fr_FR.UTF-8; KEYMAP=fr-latin1; XKB_LAYOUT=fr ;; + "Spain") TIMEZONE=Europe/Madrid; LOCALE=es_ES.UTF-8; KEYMAP=es; XKB_LAYOUT=es ;; + "Italy") TIMEZONE=Europe/Rome; LOCALE=it_IT.UTF-8; KEYMAP=it; XKB_LAYOUT=it ;; + "Netherlands") TIMEZONE=Europe/Amsterdam; LOCALE=nl_NL.UTF-8; KEYMAP=us; XKB_LAYOUT=us ;; + *) + # Other (advanced): the previous search-based prompts. Enter '?text' to search. + while true; do + cread TIMEZONE "Timezone [Europe/London] ('?text' searches): " + TIMEZONE="${TIMEZONE:-Europe/London}" + if [[ "$TIMEZONE" == \?* ]]; then + timedatectl list-timezones 2>/dev/null | grep -i -- "${TIMEZONE#\?}" | head -25 | sed 's/^/ /' + continue + fi + [ -f "/usr/share/zoneinfo/$TIMEZONE" ] && break + cecho "Not found (e.g. Europe/London, America/New_York, Australia/Sydney)." + done + while true; do + cread LOCALE "Locale [en_GB.UTF-8] ('?text' searches): " + LOCALE="${LOCALE:-en_GB.UTF-8}" + if [[ "$LOCALE" == \?* ]]; then + grep -oE '^#?[^ ]+ ' /etc/locale.gen | tr -d '# ' | grep -i -- "${LOCALE#\?}" | head -25 | sed 's/^/ /' + continue + fi + grep -oE '^#?[^ ]+ ' /etc/locale.gen | tr -d '# ' | grep -qxF "$LOCALE" && break + cecho "Not found (e.g. en_GB.UTF-8, en_US.UTF-8, de_DE.UTF-8)." + done + while true; do + cread KEYMAP "Console keymap [uk] ('?text' searches): " + KEYMAP="${KEYMAP:-uk}" + if [[ "$KEYMAP" == \?* ]]; then + localectl list-keymaps 2>/dev/null | grep -i -- "${KEYMAP#\?}" | head -25 | sed 's/^/ /' + continue + fi + localectl list-keymaps 2>/dev/null | grep -qxF "$KEYMAP" && break + cecho "Not found (e.g. uk, us, de-latin1, fr-latin1)." + done + # Map the console keymap to an XKB layout for Hyprland's kb_layout. The names + # usually agree (us, de, it, es, pl...); this table covers the common + # exceptions, and the fallback strips variant suffixes (cz-qwertz -> cz). + case "$KEYMAP" in + uk) XKB_LAYOUT="gb" ;; + fr-latin1|fr-latin9) XKB_LAYOUT="fr" ;; + pt-latin1|pt-latin9) XKB_LAYOUT="pt" ;; + br-abnt2) XKB_LAYOUT="br" ;; + la-latin1) XKB_LAYOUT="latam" ;; + sv-latin1) XKB_LAYOUT="se" ;; + sg|sg-latin1) XKB_LAYOUT="ch" ;; + dvorak*) XKB_LAYOUT="us" ;; + *) XKB_LAYOUT="${KEYMAP%%[-.]*}" ;; + esac + ;; +esac +cecho "Region set: ${TIMEZONE} · ${LOCALE} · keymap ${KEYMAP}" "$NSI_DIM" "$NSI_R" + +# Disk — arrow-key pick from the real disks, then a plain Yes/No confirm +# (defaults to No). No more typing the device path. +echo +mapfile -t DISKS < <(lsblk -d -n -p -o NAME,SIZE,MODEL | grep -Ev '/dev/(loop|sr|zram|fd)') +if [ "${#DISKS[@]}" -eq 0 ]; then + cecho "No disks found. Dropping to shell." + exec /bin/bash +fi +while true; do + DISK_LABELS=() + for d in "${DISKS[@]}"; do + DISK_LABELS+=("$(awk '{name=$1; size=$2; $1=""; $2=""; sub(/^[[:space:]]+/,""); printf "%-16s %-9s %s", name, size, $0}' <<<"$d")") + done + tui_menu "Install to which disk? (everything on it is erased)" "${DISK_LABELS[@]}" + DISK=$(awk '{print $1}' <<<"${DISKS[$MENU_CHOICE]}") + tui_menu "Erase $DISK and install NoSignal?" \ + "No — choose a different disk" \ + "Yes — ERASE $DISK and install" + [ "$MENU_CHOICE" -eq 1 ] && break +done + +if [[ "$DISK" =~ nvme|mmcblk ]]; then + EFI_PART="${DISK}p1"; ROOT_PART="${DISK}p2" +else + EFI_PART="${DISK}1"; ROOT_PART="${DISK}2" +fi + +# ---------------------------------------------------------------- install ---- +# Go quiet + centred for the heavy phase: the prompts are done, so hide every +# command's output to the log and show a single centred spinner with the current +# phase name (consistent with the centred menus, no formatting/build-hook spam). +clear +# Silence kernel printk on the console — disk format/mount triggers harmless +# btrfs/ext4 probe messages ("VFS: Can't find ext4 filesystem") that write +# straight to the VT and would splatter over the clean spinner. (dmesg keeps +# them; this only stops console printing. Not restored — a reboot follows.) +dmesg -D 2>/dev/null || echo 1 > /proc/sys/kernel/printk 2>/dev/null || true +tcecho "Installing NoSignal" "$NSI_GB" "$NSI_R" +tcecho "this takes a few minutes — sit tight" "$NSI_DIM" "$NSI_R" +printf '\n' >/dev/tty +nsi_phase "Preparing the disk" +exec >>/tmp/installer.log 2>&1 +trap nsi_fail ERR +nsi_spin_start +echo "==> Wiping $DISK..." +swapoff -a || true +umount -R /mnt 2>/dev/null || true +wipefs -af "$DISK" +sgdisk --zap-all "$DISK" + +echo "==> Partitioning (1G EFI + rest btrfs)..." +# 1 GiB ESP (was 512M): UKIs are self-contained kernel+initramfs+microcode, and +# the NVIDIA UKI is ~139 MB (driver + GSP firmware). With ENABLE_LIMINE_FALLBACK +# (a 2nd UKI) plus multiple kernel versions, a 512 MB ESP can fill and UKI writes +# then fail (finding F4 secondary risk). 1 GiB is safe headroom on every GPU. +sgdisk -n1:0:+1G -t1:ef00 -c1:EFI "$DISK" +sgdisk -n2:0:0 -t2:8300 -c2:ROOT "$DISK" +partprobe "$DISK" +udevadm settle + +echo "==> Formatting (btrfs root for Limine snapshots)..." +mkfs.fat -F32 -n EFI "$EFI_PART" +mkfs.btrfs -f -L ROOT "$ROOT_PART" + +# Btrfs subvolume layout for snapshot/rollback (root-only snapshots): +# @ -> / (snapshotted + rolled back) +# @home -> /home (separate, so a rollback never reverts user data) +# @snapshots -> /.snapshots (separate, so snapshots survive a rollback of @) +# @log -> /var/log (separate, so logs survive a rollback — useful) +echo "==> Creating btrfs subvolumes..." +mount "$ROOT_PART" /mnt +btrfs subvolume create /mnt/@ +btrfs subvolume create /mnt/@home +btrfs subvolume create /mnt/@snapshots +btrfs subvolume create /mnt/@log +umount /mnt + +echo "==> Mounting subvolumes..." +BTRFS_OPTS="noatime,compress=zstd,space_cache=v2" +mount -o "$BTRFS_OPTS,subvol=@" "$ROOT_PART" /mnt +mkdir -p /mnt/boot /mnt/home /mnt/.snapshots /mnt/var/log +mount -o "$BTRFS_OPTS,subvol=@home" "$ROOT_PART" /mnt/home +mount -o "$BTRFS_OPTS,subvol=@snapshots" "$ROOT_PART" /mnt/.snapshots +mount -o "$BTRFS_OPTS,subvol=@log" "$ROOT_PART" /mnt/var/log +mount "$EFI_PART" /mnt/boot + +# ------------------------------------------------------------ GPU drivers ---- +# Hardware-detect the GPU(s) and pick driver packages, borrowing CachyOS chwd's +# method: scan PCI display-class controllers (class 0300/0302/0380) and map the +# vendor id (10de NVIDIA / 1002 AMD / 8086 Intel) to a driver set — plus the VM +# vendors so Hyprland comes up in a guest. Done natively (no chwd binary, no +# CachyOS repos / lib32 / cachyos-kernel deps) so the ISO stays vanilla Arch. +# Multiple GPUs union their package sets (e.g. an Intel/AMD iGPU + NVIDIA dGPU). +# ALL variants below are present in the offline repo (see GPU_ALL_PKGS in the +# builder — keep the two in sync). +echo "==> Detecting GPU(s)..." +GPU_NVIDIA=0; GPU_AMD=0; GPU_INTEL=0; GPU_VM=0 +IGPU_PCI_ID="" # vendor:device of the iGPU (for hybrid Vulkan device steering) +while IFS= read -r gpuline; do + case "$gpuline" in + *'[10de:'*) GPU_NVIDIA=1 ;; + *'[1002:'*) GPU_AMD=1; [ -z "$IGPU_PCI_ID" ] && IGPU_PCI_ID=$(printf '%s\n' "$gpuline" | grep -oE '1002:[0-9a-fA-F]{4}' | head -1) ;; + *'[8086:'*) GPU_INTEL=1; [ -z "$IGPU_PCI_ID" ] && IGPU_PCI_ID=$(printf '%s\n' "$gpuline" | grep -oE '8086:[0-9a-fA-F]{4}' | head -1) ;; + *'[1af4:'*|*'[1b36:'*|*'[1234:'*|*'[15ad:'*|*'[80ee:'*) GPU_VM=1 ;; + esac +done < <(lspci -nn 2>/dev/null | grep -Ei 'VGA compatible controller|3D controller|Display controller') + +# Hybrid-laptop awareness. An iGPU (Intel/AMD) present ALONGSIDE an NVIDIA dGPU on +# a LAPTOP is almost always muxless Optimus: the internal panel is wired to the +# iGPU and the dGPU is only a secondary render device. Forcing the NVIDIA GLX/ +# VA-API globally — correct when the display hangs off the NVIDIA card (desktop, +# or a laptop MUX set to discrete) — then breaks/slows the iGPU-driven Wayland +# session and keeps the dGPU awake on battery. So on a hybrid laptop we keep the +# iGPU primary, ship prime-run for on-demand offload, and enable dGPU runtime +# power management, while STILL installing the NVIDIA driver + KMS so offload +# works. Desktops with NVIDIA present are unaffected (no laptop chassis/battery). +# Override the heuristic with NOSIGNAL_GPU_PRIMARY=nvidia|igpu|auto (default auto). +GPU_HYBRID_LAPTOP=0 +IGPU_VAAPI=radeonsi +if [ "$GPU_NVIDIA" = 1 ] && { [ "$GPU_INTEL" = 1 ] || [ "$GPU_AMD" = 1 ]; }; then + is_laptop=0 + ct=$(cat /sys/class/dmi/id/chassis_type 2>/dev/null || echo 0) + case "$ct" in 8|9|10|11|14|30|31|32) is_laptop=1 ;; esac + for b in /sys/class/power_supply/BAT*; do [ -e "$b" ] && is_laptop=1; done + case "${NOSIGNAL_GPU_PRIMARY:-auto}" in + igpu) GPU_HYBRID_LAPTOP=1 ;; + nvidia) GPU_HYBRID_LAPTOP=0 ;; + *) GPU_HYBRID_LAPTOP=$is_laptop ;; + esac + # iGPU VA-API backend (the panel-connected decoder): Intel -> iHD, AMD -> radeonsi. + [ "$GPU_INTEL" = 1 ] && IGPU_VAAPI=iHD +fi + +GPU_PKGS=(mesa vulkan-icd-loader) +GPU_SUMMARY=() +if [ "$GPU_NVIDIA" = 1 ]; then + # Open-kernel-module NVIDIA (Turing/RTX+; the current default). DKMS so it + # tracks the stock 'linux' kernel; linux-headers builds it during pacstrap. + GPU_PKGS+=(nvidia-open-dkms nvidia-utils egl-wayland libva-nvidia-driver nvidia-settings linux-headers) + GPU_SUMMARY+=("NVIDIA (nvidia-open-dkms + Wayland KMS)") +fi +if [ "$GPU_AMD" = 1 ]; then + GPU_PKGS+=(vulkan-radeon) + GPU_SUMMARY+=("AMD (mesa / vulkan-radeon)") +fi +if [ "$GPU_INTEL" = 1 ]; then + GPU_PKGS+=(vulkan-intel intel-media-driver) + GPU_SUMMARY+=("Intel (mesa / vulkan-intel)") +fi +if [ "$GPU_VM" = 1 ]; then + GPU_PKGS+=(qemu-guest-agent spice-vdagent) + GPU_SUMMARY+=("VM guest (virtio / qemu)") +fi +if [ "${#GPU_SUMMARY[@]}" -eq 0 ]; then + # Nothing recognised — software rendering keeps Hyprland able to start. + GPU_PKGS+=(vulkan-swrast) + GPU_SUMMARY+=("unknown — software fallback (vulkan-swrast)") +fi +if [ "$GPU_HYBRID_LAPTOP" = 1 ]; then + # prime-run for on-demand dGPU offload; iGPU stays primary (see env block below). + GPU_PKGS+=(nvidia-prime) + GPU_SUMMARY+=("hybrid laptop -> iGPU primary + NVIDIA PRIME offload (prime-run)") +fi +echo " Driver target(s): ${GPU_SUMMARY[*]}" + +nsi_phase "Installing the NoSignal desktop (this is the slow part)" +pacstrap -C /tmp/nosignal-pacman.conf -K /mnt "${BASE_PKGS[@]}" "${GPU_PKGS[@]}" + +echo "==> Generating fstab..." +genfstab -U /mnt >> /mnt/etc/fstab + +# F7: genfstab inherits the ESP's live mount options (fmask=0022,dmask=0022), +# which make /boot world-readable — exposing the bootloader random-seed and +# limine.conf (kernel cmdline, root PARTUUID, snapshot history) to any local +# user. systemd's bootctl flags this at every boot as a security hole. Tighten +# the vfat (ESP) line to root rw / group r / others none: files rw-r----- and +# dirs rwxr-x---. (Only the ESP is vfat here, so the vfat-line match is exact.) +sed -i -E '/[[:space:]]vfat[[:space:]]/{ s/fmask=[0-7]+/fmask=0137/; s/dmask=[0-7]+/dmask=0027/ }' /mnt/etc/fstab +if grep -qE '[[:space:]]vfat[[:space:]].*fmask=0137' /mnt/etc/fstab; then + echo " ESP hardened (fmask=0137,dmask=0027 — not world-readable)." +else + echo " WARNING: could not harden ESP mount perms in fstab — review /boot fmask/dmask." +fi + +nsi_phase "Configuring the system" +echo "==> Configuring base system in chroot..." +# Localisation from the prompts (written from outside the chroot — simpler +# than threading variables through a quoted heredoc). +ln -sf "/usr/share/zoneinfo/$TIMEZONE" /mnt/etc/localtime +sed -i "s|^#${LOCALE} |${LOCALE} |" /mnt/etc/locale.gen +# en_US.UTF-8 as an always-present fallback locale. +sed -i 's|^#en_US.UTF-8 UTF-8|en_US.UTF-8 UTF-8|' /mnt/etc/locale.gen +echo "LANG=$LOCALE" > /mnt/etc/locale.conf +echo "KEYMAP=$KEYMAP" > /mnt/etc/vconsole.conf +# Resolve .local hostnames via Avahi (printers, other LAN boxes): insert +# mdns_minimal before the resolve/dns sources, the standard nss-mdns setup. +sed -i '/^hosts:/ s/resolve/mdns_minimal [NOTFOUND=return] resolve/' /mnt/etc/nsswitch.conf +arch-chroot /mnt /bin/bash <<'CHROOT' +set -e +hwclock --systohc +locale-gen +sed -i 's/^# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) ALL/' /etc/sudoers +systemctl enable NetworkManager.service +systemctl enable bluetooth.service +systemctl enable cups.socket +systemctl enable avahi-daemon.service +systemctl enable power-profiles-daemon.service +# Process sysusers so the 'sddm' system user exists (pacstrap doesn't run +# the package's sysusers.d automatically). Harmless if already present. +systemd-sysusers || true +CHROOT + +# --------------------------------------------------------------- Plymouth ---- +# Boot splash between Limine and the SDDM greeter: insert the `plymouth` +# initramfs hook right after the init manager hook and set the NoSignal theme +# (the "NO SIGNAL / 入力信号なし" logo centred on black). With the quiet/splash +# kernel cmdline, the boot shows that splash instead of scrolling kernel text. +echo "==> Configuring Plymouth (NoSignal boot splash)..." +# Ship the NoSignal Plymouth theme (vendored). Root dir is 'nosignal' -> +# /usr/share/plymouth/themes/nosignal. Must land BEFORE the initramfs rebuild +# below so the theme's images/script are embedded in the initramfs. +install -d -m 755 /mnt/usr/share/plymouth/themes +tar -xzf "$NOSIGNAL_PAYLOAD/vendor/nosignal-plymouth.tar.gz" \ + -C /mnt/usr/share/plymouth/themes +# The plymouth hook goes right after the init manager hook. Arch's default +# mkinitcpio.conf now uses the `systemd` hook (not `udev`), so handle both: +# plymouth after `systemd` if present, else after `udev`. Idempotent. +if ! grep -qE '^HOOKS=.*\bplymouth\b' /mnt/etc/mkinitcpio.conf; then + if grep -qE '^HOOKS=.*\bsystemd\b' /mnt/etc/mkinitcpio.conf; then + sed -i '/^HOOKS=/ s/\bsystemd\b/systemd plymouth/' /mnt/etc/mkinitcpio.conf + else + sed -i '/^HOOKS=/ s/\budev\b/udev plymouth/' /mnt/etc/mkinitcpio.conf + fi +fi +# Set NoSignal as the default theme (no -R: the mkinitcpio -P below rebuilds the +# initramfs and embeds it). Falls back to the built-in spinner if it can't apply. +arch-chroot /mnt plymouth-set-default-theme nosignal \ + || arch-chroot /mnt plymouth-set-default-theme spinner || true + +# ------------------------------------------------------------- NVIDIA KMS ---- +# Borrowed from chwd's pre_install hook: early-load the NVIDIA modules and drop +# the 'kms' hook (which would otherwise bind nouveau to the card), enable DRM +# modeset, and set the Wayland EGL/VA env. This is what actually lets Hyprland +# start on NVIDIA. No-op on AMD/Intel/VM-only systems. +if [ "$GPU_NVIDIA" = 1 ]; then + echo "==> Configuring NVIDIA DRM/KMS for Wayland (chwd-style)..." + install -d -m 755 /mnt/etc/mkinitcpio.conf.d + cat > /mnt/etc/mkinitcpio.conf.d/10-nosignal-nvidia.conf <<'NV' +# Generated by NoSignal. Early-load the NVIDIA modules for KMS. +MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm) +NV + cat > /mnt/etc/mkinitcpio.conf.d/20-nosignal-nvidia-kms.conf <<'NVK' +# Generated by NoSignal. Drop the 'kms' hook so nouveau isn't pulled in early. +HOOKS=(${HOOKS[@]/kms/}) +NVK + cat > /mnt/etc/modprobe.d/nvidia.conf <<'NVMOD' +options nvidia_drm modeset=1 fbdev=1 +NVMOD + # System-wide EGL/VA env. caelestia symlinks ~/.config/hypr, so per-user hypr + # env files would be clobbered — /etc/environment is read for the session. + if [ "$GPU_HYBRID_LAPTOP" = 1 ]; then + # Muxless Optimus: the iGPU drives the panel. Do NOT force NVIDIA GLX/VA-API + # globally — that would push the whole session onto the dGPU. Leave GLX to the + # iGPU, point VA-API at the iGPU decoder, and let apps opt into the dGPU with + # `prime-run`. Enable dGPU runtime power management (D3) so it sleeps when idle + # (Turing+; nvidia-open supports it — matches the nvidia-open-dkms gate). + cat >> /mnt/etc/environment <> /mnt/etc/environment < /mnt/etc/modprobe.d/nvidia-pm.conf <<'NVPM' +# Fine-grained dGPU runtime power management for Optimus battery life (Turing+). +options nvidia NVreg_DynamicPowerManagement=0x02 +NVPM + install -d -m 755 /mnt/etc/udev/rules.d + cat > /mnt/etc/udev/rules.d/80-nosignal-nvidia-pm.rules <<'NVUDEV' +# Let the NVIDIA dGPU autosuspend when idle (runtime D3). VGA + 3D controllers. +# +# RTD3 Cause 1 (2026-06-20 hardware test, Acer AMD iGPU + RTX 3050): the nvidia +# driver binds the dGPU INSIDE the initramfs (mkinitcpio `kms` hook pulls it in +# even with MODULES=()), before real-root udevd exists. Coldplug then replays the +# device as ACTION=="add", which an ACTION=="bind" rule never matched, leaving +# power/control at the kernel default "on" (dGPU never reached D3, ~4 W idle). +# DRIVER=nvidia is already set at coldplug, so match the bound device with NO +# ACTION gate. Keep restoring "auto" on unbind for a later rebind/probe. +SUBSYSTEM=="pci", DRIVERS=="nvidia", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="auto" +SUBSYSTEM=="pci", DRIVERS=="nvidia", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="auto" +ACTION=="unbind", SUBSYSTEM=="pci", DRIVERS=="nvidia", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="auto" +ACTION=="unbind", SUBSYSTEM=="pci", DRIVERS=="nvidia", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="auto" +NVUDEV + else + # NVIDIA drives the display (desktop, or a laptop MUX set to discrete): force + # the NVIDIA GLX/VA-API session-wide. + cat >> /mnt/etc/environment <<'NVENV' +# --- NoSignal NVIDIA Wayland env --- +LIBVA_DRIVER_NAME=nvidia +__GLX_VENDOR_LIBRARY_NAME=nvidia +NVD_BACKEND=direct +NVENV + fi +fi + +# ---------------------------------------------------------- Acer power ------- +# Acer Predator/Nitro laptops keep the dGPU's higher power states LOCKED until +# acer_wmi loads with predator_v4=1 — then the NitroSense/Turbo key cycles them +# (e.g. 35->40->50->60 W). NoSignal didn't set this out of the box, so the dGPU +# was capped low (hardware test 2026-06-20). Bake it in, gated on Acer hardware +# AND a kernel acer_wmi that accepts the option, so non-Acer machines are +# untouched. nvidia-powerd (Dynamic Boost) is enabled when an NVIDIA driver is +# present (the unit only exists then). Platform profile stays the Omarchy default +# (balanced) — the user can raise it. Source: 28allday/Acer-Power-Control-Omarchy. +SYS_VENDOR=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "") +if printf '%s' "$SYS_VENDOR" | grep -qi acer \ + && modinfo -p acer_wmi 2>/dev/null | grep -q "^predator_v4:"; then + echo "==> Acer laptop detected — unlocking Predator/Nitro GPU power (predator_v4=1)..." + printf 'options acer_wmi predator_v4=1\n' > /mnt/etc/modprobe.d/acer-wmi.conf + if [ "$GPU_NVIDIA" = 1 ]; then + arch-chroot /mnt systemctl enable nvidia-powerd >/dev/null 2>&1 \ + && echo " nvidia-powerd enabled (Dynamic Boost)" \ + || echo " nvidia-powerd not enabled (optional)" + fi +fi + +nsi_phase "Installing the bootloader" +echo "==> Installing Limine bootloader (UEFI)..." +ROOT_PARTUUID=$(blkid -s PARTUUID -o value "$ROOT_PART") +# Root is the btrfs @ subvolume — Limine/kernel needs rootflags to mount it. +# The quiet/splash flags give a clean boot: Plymouth shows the NoSignal splash +# (NO SIGNAL logo on black) and the kernel/systemd "loading" text is suppressed, +# so the boot goes Limine -> NoSignal splash -> SDDM greeter with no scroll. +KERNEL_OPTS="root=PARTUUID=$ROOT_PARTUUID rw rootflags=subvol=@" +KERNEL_OPTS="$KERNEL_OPTS quiet splash loglevel=3 systemd.show_status=false rd.udev.log_level=3 vt.global_cursor_default=0" +# Disable zswap: the swap device is zram (already compressed RAM), so zswap in +# front of it would double-compress pages (zswap -> zram) and waste CPU. +KERNEL_OPTS="$KERNEL_OPTS zswap.enabled=0" +# NVIDIA needs DRM modeset on the cmdline too (belt-and-suspenders with the +# modprobe.d option) for Wayland/Hyprland. +[ "$GPU_NVIDIA" = 1 ] && KERNEL_OPTS="$KERNEL_OPTS nvidia_drm.modeset=1" + +# /mnt/boot is the FAT32 ESP. Drop Limine's UEFI binary at BOTH the spec-defined +# vendor path (/EFI/limine) and the removable-media fallback (/EFI/BOOT/BOOTX64 +# .EFI). The fallback is what makes the disk boot without an NVRAM entry — the +# efibootmgr call below often can't write NVRAM (chroot / VMs / locked firmware), +# so the fallback path is the reliable one; the NVRAM entry is a nicety. +install -d /mnt/boot/EFI/limine /mnt/boot/EFI/BOOT +cp /mnt/usr/share/limine/BOOTX64.EFI /mnt/boot/EFI/limine/BOOTX64.EFI +cp /mnt/usr/share/limine/BOOTX64.EFI /mnt/boot/EFI/BOOT/BOOTX64.EFI + +# limine.conf lives on the ESP root; boot():/ resolves to that same ESP, where +# the kernel/ucode/initramfs all sit (since /boot IS the ESP). Microcode modules +# must precede the main initramfs. +# timeout: 10 — keep the Limine menu up 10s before auto-booting (user choice; +# the limine snapshot tooling only manages entries, it never rewrites this). +# +# F4 (dead-boot after a kernel update): with ENABLE_UKI=yes the first kernel +# update DELETES /boot/vmlinuz-linux + initramfs-linux.img (they're folded into +# the UKI). The OLD seed made the *first/default* manual entry `protocol: linux` +# pointing at those now-missing files, with no `default_entry:` — so once the +# UKI hook ran, the default auto-boot target dead-booted to a TTY. The previous +# post-install conversion mis-fired (ran once, no-op'd, marked applied), so the +# durable fix lives HERE in the seed: +# * entry 1 = `protocol: efi` -> the UKI (boots once the UKI exists, which is +# at install in the normal path, or at worst after the first kernel update). +# * entry 2 = a `protocol: linux` fallback for the rare pre-UKI first boot +# (snapshot/limine-update failed at install, so no UKI yet). Becomes a no-op +# stub once the UKI exists; harmless, never the default. +# * `default_entry: 1` pins the UKI entry as default regardless of the auto +# entries limine appends below it on every kernel update (limine preserves +# these manual entries + general directives verbatim across regenerations, +# so this seed is durable — no per-update hook needed). +cat > /mnt/boot/limine.conf </dev/null || true + +# ------------------------------------------------- Btrfs + Limine snapshots --- +# Omarchy-style bootable snapshots, configured AT INSTALL TIME (this all lived +# in the first-boot script before Phase 3): limine-snapper-sync + the UKI-based +# limine-mkinitcpio-hook + snapper (root only). After this, every pacman +# transaction auto-snapshots (snap-pac), and each snapshot shows as a bootable +# entry in the Limine menu — pick one to roll back a bad update. Resilient: if +# any of this fails, the basic Limine boot set up above still works. +echo "==> Configuring Btrfs + Limine snapshots..." + +# /etc/default/limine is read by the limine tools to build UKIs + boot entries. +# It must exist BEFORE the tools are installed/run. Carries the same cmdline. +cat > /mnt/etc/default/limine < /mnt/etc/mkinitcpio.conf.d/10-nosignal-btrfs.conf +fi + +# The snapshot tools come PREBUILT from the omarchy repo (bundled in the +# offline repo) — installed in a second pacstrap now that their config exists. +# KEEP IN SYNC with LIMINE_TOOLS in the builder. +nsi_phase "Installing snapshot + boot tooling" +pacstrap -C /tmp/nosignal-pacman.conf /mnt limine-snapper-sync limine-mkinitcpio-hook + +# snapper "root" config. /.snapshots is already a mounted @snapshots subvol, +# which collides with snapper create-config (it wants to make that subvol) — +# so do the documented dance: unmount, let snapper create its own, delete it, +# then remount our @snapshots. --no-dbus because there's no snapperd in chroot. +SNAPSHOTS_OK=0 +if umount /mnt/.snapshots \ + && rm -rf /mnt/.snapshots \ + && arch-chroot /mnt snapper --no-dbus -c root create-config / \ + && arch-chroot /mnt btrfs subvolume delete /.snapshots \ + && mkdir /mnt/.snapshots \ + && mount -o "$BTRFS_OPTS,subvol=@snapshots" "$ROOT_PART" /mnt/.snapshots \ + && chmod 750 /mnt/.snapshots; then + # Root-only, keep 5, no timeline (pre/post pacman snapshots via snap-pac). + arch-chroot /mnt snapper --no-dbus -c root set-config NUMBER_LIMIT=5 NUMBER_LIMIT_IMPORTANT=5 TIMELINE_CREATE=no || true + arch-chroot /mnt btrfs quota disable / 2>/dev/null || true # qgroup accounting is a perf drag + arch-chroot /mnt systemctl enable limine-snapper-sync.service || true + SNAPSHOTS_OK=1 + echo " Snapshots configured." +else + echo " snapper setup failed — snapshots disabled (basic Limine boot unaffected)." +fi + +# Rebuild the initramfs once, now that Plymouth, any NVIDIA overrides and the +# btrfs-overlayfs hook are all in place — then let the limine tooling generate +# the UKIs and the final boot entries. +# Full path: limine-mkinitcpio-hook ships a /usr/local/bin/mkinitcpio wrapper +# that PROMPTS [Y/n] after -P ("run limine-mkinitcpio now?") — an interactive +# blocker mid-install. limine-update below regenerates the entries anyway. +nsi_phase "Building the boot image" +arch-chroot /mnt /usr/bin/mkinitcpio -P +if [ "$SNAPSHOTS_OK" = 1 ]; then + if arch-chroot /mnt limine-update; then + echo " Limine UKI entries generated." + else + echo " limine-update failed — basic Limine boot still works." + fi +fi + +# ---------------------------------------------------------------- firewall --- +# Default deny inbound, allow outbound (a desktop needs no open inbound ports). +# ufw in a chroot can't apply rules to netfilter and returns non-zero on some +# versions — tolerate that; ENABLED=yes in ufw.conf + the enabled service make +# the policy apply at boot. +echo "==> Configuring firewall (ufw)..." +arch-chroot /mnt ufw default deny incoming || true +arch-chroot /mnt ufw default allow outgoing || true +# omarchy-send / LocalSend protocol (TCP transfers + UDP discovery). +arch-chroot /mnt ufw allow 53317 || true +# mDNS responses (Avahi: .local resolution, printer/device discovery). +arch-chroot /mnt ufw allow 5353/udp || true +sed -i 's/^ENABLED=no/ENABLED=yes/' /mnt/etc/ufw/ufw.conf 2>/dev/null || true +arch-chroot /mnt systemctl enable ufw.service || true + +echo "==> Writing hostname / hosts..." +echo "$HOSTNAME" > /mnt/etc/hostname +cat > /mnt/etc/hosts < Creating user $USERNAME..." +# video/audio/input groups for a desktop session; wheel for sudo. +arch-chroot /mnt useradd -m -G wheel,video,audio,input -s /bin/bash "$USERNAME" +printf 'root:%s\n%s:%s\n' "$USER_PW" "$USERNAME" "$USER_PW" | arch-chroot /mnt chpasswd + +# ----------------------------------------------------------- ssh handoff ---- +# SSH is OPTIONAL on a desktop. If the build host baked a public key we install +# it and enable sshd (handy for remote rescue / setup), opening 22 in ufw. +# Otherwise sshd stays disabled — this is a desktop, not a server. +if [ -f /root/master.pub ]; then + echo "==> Installing master SSH key for $USERNAME + enabling sshd..." + install -m 700 -d "/mnt/home/$USERNAME/.ssh" + install -m 600 /root/master.pub "/mnt/home/$USERNAME/.ssh/authorized_keys" + arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "/home/$USERNAME/.ssh" + install -m 755 -d /mnt/etc/ssh/sshd_config.d + cat > /mnt/etc/ssh/sshd_config.d/10-key-only.conf <<'SSHD' +PasswordAuthentication no +KbdInteractiveAuthentication no +PermitRootLogin no +SSHD + arch-chroot /mnt systemctl enable sshd.service || true + arch-chroot /mnt ufw allow 22/tcp || true +else + echo " (no /root/master.pub on the ISO — sshd left disabled)" +fi + +# ------------------------------------------------- online repos (post-boot) -- +# The INSTALLED system uses the normal online mirrors, not the install media. +# The archiso live mirrorlist is reflector-generated only when online — on an +# offline install it can be empty/commented, so write a sane default the user +# can refine later. NB: nothing is synced yet; the user's first package +# operation should be `sudo pacman -Syu`. +echo "==> Writing pacman mirrorlist for the installed system..." +cat > /mnt/etc/pacman.d/mirrorlist <<'MIRROR' +## NoSignal default mirror (geo-routed). Refine with reflector if you like: +## sudo reflector --latest 10 --sort rate --save /etc/pacman.d/mirrorlist +Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch +MIRROR + +# The omarchy repo serves PREBUILT updates for the Limine snapshot tools +# (otherwise they rebuild via a ~530MB GraalVM toolchain from the AUR). +# NB: SigLevel is Optional TrustAll (unsigned) — same as Omarchy uses; it means +# trusting omarchy's binaries. Only the two limine-* packages come from it. +echo "==> Adding omarchy repo (prebuilt Limine snapshot tool updates)..." +cat >> /mnt/etc/pacman.conf <<'OMARCHY_REPO' + +[omarchy] +SigLevel = Optional TrustAll +Server = https://pkgs.omarchy.org/edge/$arch +OMARCHY_REPO + +# Enable [multilib] (layer change 15 — gaming enablement): the lib32-* deps +# Steam/gaming installers (DeckShift) need live there. Uncomments ONLY the +# [multilib] block (leaves [multilib-testing] alone). Nothing is synced here — +# the install is offline; the user's first `pacman -Syu` pulls the db. +echo "==> Enabling [multilib] repo in the installed system..." +sed -i '/^#\[multilib\]/,/^#Include/ s/^#//' /mnt/etc/pacman.conf + +# ---------------------------------------------------------- zram swap -------- +# A desktop with NO swap OOM-kills heavy workloads; zram gives compressed +# in-RAM swap (no disk partition needed). zram-generator is a systemd +# generator: the package + this config auto-activate systemd-zram-setup@zram0 +# at boot, no explicit enable needed. +echo "==> Configuring zram swap..." +cat > /mnt/etc/systemd/zram-generator.conf <<'ZRAM' +[zram0] +zram-size = min(ram, 8192) +compression-algorithm = zstd +ZRAM +chmod 644 /mnt/etc/systemd/zram-generator.conf + +# ------------------------------------- CachyOS-derived performance tweaks ---- +# Cherry-picked from CachyOS-Settings (github.com/CachyOS/CachyOS-Settings), +# same borrow-the-method approach as the chwd GPU detection: a curated subset +# of their sysctl + udev rules, vendored as plain text (no CachyOS repos or +# kernel). swappiness=100 is correct WITH zram (compressed swap is cheap). +echo "==> Applying CachyOS-derived performance tweaks..." +cat > /mnt/etc/sysctl.d/99-nosignal-performance.conf <<'SYSCTL' +# NoSignal — desktop responsiveness tweaks (curated from CachyOS-Settings). +kernel.nmi_watchdog = 0 +kernel.split_lock_mitigate = 0 +vm.max_map_count = 2147483642 +vm.swappiness = 100 +vm.vfs_cache_pressure = 50 +# With zram (RAM-fast, per-page decompression) swap read-ahead just wastes CPU, +# so disable it (Arch Wiki / Fedora recommend page-cluster=0 with zram). +vm.page-cluster = 0 +net.ipv4.tcp_fastopen = 3 +SYSCTL +cat > /mnt/etc/udev/rules.d/60-ioschedulers.rules <<'IOSCHED' +# NoSignal — I/O scheduler per device type (from CachyOS-Settings): +# NVMe -> none, SATA/eMMC SSD -> mq-deadline, rotational -> bfq. +ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="none" +ACTION=="add|change", KERNEL=="sd[a-z]*|mmcblk[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline" +ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq" +IOSCHED + +# ---------------------------------------------- ~/.local/bin on PATH -------- +# Put ~/.local/bin onto every login shell's PATH so user-installed binaries are +# runnable without manual PATH edits. /etc/profile.d/*.sh is sourced by +# /etc/profile for any login shell, before user dotfiles. +echo "==> Adding ~/.local/bin to PATH for all login shells..." +install -d -m 755 /mnt/etc/profile.d +cat > /mnt/etc/profile.d/local-bin.sh <<'PROFILE_LOCAL_BIN' +# Add ~/.local/bin to PATH for login shells (idempotent). +case ":$PATH:" in + *":$HOME/.local/bin:"*) ;; + *) [ -d "$HOME/.local/bin" ] && PATH="$HOME/.local/bin:$PATH" ;; +esac +PROFILE_LOCAL_BIN +chmod 644 /mnt/etc/profile.d/local-bin.sh + +# --------------------------------------------------------------- SDDM -------- +# Display manager: SDDM (layer change 16, REQUIRED — DeckShift's +# desktop<->gaming session switching rewrites SDDM config + restarts it; +# greetd/tuigreet couldn't do this). Password greeter, NO autologin baked — +# DeckShift manages its own autologin/session entries post-install (change 17, +# opt-in). Canonical source: the layer's display-manager-sddm/ +# sddm-10-nosignal.conf; mirrored here to keep the installer self-contained. +echo "==> Writing + enabling SDDM..." +install -d -m 755 /mnt/etc/sddm.conf.d +cat > /mnt/etc/sddm.conf.d/10-nosignal.conf <<'SDDMCONF' +# NoSignal SDDM config (installed to /etc/sddm.conf.d/10-nosignal.conf). +[General] +# X11 greeter — the reliable, well-trodden path (needs xorg-server). The Hyprland +# SESSION still runs on Wayland; only SDDM's own greeter uses X here. +# For an X-free image instead, use the Wayland greeter: +# DisplayServer=wayland +# [Wayland] +# CompositorCommand=weston --shell=kiosk (adds `weston` to the image) +DisplayServer=x11 +Numlock=on + +[Users] +# The desktop session to prefer. hyprland-uwsm.desktop launches Hyprland through +# uwsm exactly like the old greetd config — keep this (NOT plain hyprland.desktop), +# or caelestia's uwsm/systemd-managed session env won't apply. +# (SDDM remembers the last-used session per user; DeckShift overrides this when +# switching to/from its Gamescope gaming session.) +SDDMCONF +chmod 644 /mnt/etc/sddm.conf.d/10-nosignal.conf +# Seed SDDM's remembered last user/session so the very first greeter already +# preselects the uwsm-managed Hyprland session (hyprland-uwsm.desktop — the +# same canonical uwsm launch the old tuigreet --cmd used; plain +# hyprland.desktop would lose caelestia's uwsm/systemd-managed session env). +# SDDM rewrites this file itself after every login. mkdir -p (not install -d): +# the sddm package owns /var/lib/sddm with its own ownership/mode — don't touch. +mkdir -p /mnt/var/lib/sddm +cat > /mnt/var/lib/sddm/state.conf < Installing caelestia desktop for $USERNAME (offline)..." +USER_HOME="/home/$USERNAME" +M_HOME="/mnt$USER_HOME" + +install -d -m 755 "$M_HOME/.local/share" "$M_HOME/.config" +tar -xzf "$NOSIGNAL_PAYLOAD/vendor/caelestia-dotfiles.tar.gz" -C "$M_HOME/.local/share" +chmod u+x "$M_HOME/.local/share/caelestia/hypr/scripts/wsaction.fish" 2>/dev/null || true + +# The symlink set upstream install.fish creates (targets are the FINAL paths +# as seen on the booted system, so they dangle here in the live env — fine). +for link in hypr foot fish uwsm btop; do + [ -e "$M_HOME/.local/share/caelestia/$link" ] || continue + ln -sfn "$USER_HOME/.local/share/caelestia/$link" "$M_HOME/.config/$link" +done +[ -e "$M_HOME/.local/share/caelestia/starship.toml" ] \ + && ln -sfn "$USER_HOME/.local/share/caelestia/starship.toml" "$M_HOME/.config/starship.toml" + +# --- NoSignal restraint config (Phase 2) --------------------------------------- +# Overrides on top of caelestia's defaults. Pure config — Caelestia's own +# token/appearance system reads these; no shell code is touched, and deleting +# any file restores stock caelestia. Canonical source: the nosignal-shell config +# repo (nosignal/config/); mirrored here to keep the installer self-contained. +echo "==> Applying NoSignal desktop restraint config..." +install -d -m 755 "$M_HOME/.config/caelestia" +# Restrained design tokens: SQUARE corners (rounding ramp zeroed — redesign goal; +# the 77 Tokens.rounding.* bindings honour these, appearance.rounding.scale does +# not feed them; rounding.full omitted so genuine circles stay), tighter +# spacing/padding, denser fonts, snappier animations (overrides Caelestia's +# config-driven Tokens system). +cat > "$M_HOME/.config/caelestia/shell-tokens.json" <<'NOSIGNAL_TOKENS' +{ + "appearance": { + "rounding": { "extraSmall": 0, "small": 0, "medium": 0, "large": 0, "largeIncreased": 0, "extraLarge": 0, "extraLargeIncreased": 0, "extraExtraLarge": 0 }, + "spacing": { "extraSmall": 3, "small": 6, "medium": 9, "large": 13, "largeIncreased": 16, "extraLarge": 22, "extraLargeIncreased": 26, "extraExtraLarge": 38 }, + "padding": { "extraSmall": 3, "small": 6, "medium": 9, "large": 13, "largeIncreased": 16, "extraLarge": 22, "extraLargeIncreased": 26, "extraExtraLarge": 38 }, + "fontSize": { "small": 10, "smaller": 11, "normal": 12, "larger": 14, "large": 16, "extraLarge": 24 }, + "animDurations": { "small": 120, "normal": 200, "large": 320, "extraLarge": 450, "expressiveFastSpatial": 200, "expressiveDefaultSpatial": 300, "expressiveSlowSpatial": 400, "expressiveFastEffects": 100, "expressiveDefaultEffects": 150, "expressiveSlowEffects": 220 } + } +} +NOSIGNAL_TOKENS +# Appearance restraint + config-level cuts: opaque panels, Material Symbols +# Outlined icons, drop the Performance/Weather dashboard tabs, visualiser off. +# F9: NoSignal launches Hyprland via uwsm, but caelestia's built-in default +# session.commands.logout targets a plain (non-uwsm) session, so the session +# menu's Logout button was a no-op. `uwsm stop` is the documented way to end a +# uwsm session (drops back to SDDM); shutdown/reboot/hibernate use systemctl and +# are left at caelestia's defaults. +cat > "$M_HOME/.config/caelestia/shell.json" <<'NOSIGNAL_SHELL' +{ + "appearance": { + "transparency": { "enabled": false }, + "rounding": { "scale": 0 }, + "font": { + "headline": { "family": "JetBrainsMono Nerd Font" }, + "title": { "family": "JetBrainsMono Nerd Font" }, + "body": { "family": "JetBrainsMono Nerd Font" }, + "label": { "family": "JetBrainsMono Nerd Font" }, + "mono": { "family": "JetBrainsMono Nerd Font" }, + "icon": { "family": "Material Symbols Outlined" } + } + }, + "general": { + "apps": { + "terminal": ["kitty"], + "explorer": ["nautilus"] + } + }, + "session": { + "commands": { + "logout": ["uwsm", "stop"] + } + }, + "dashboard": { "showPerformance": false, "showWeather": true }, + "background": { "visualiser": { "enabled": false } }, + "bar": { + "entries": [ + { "id": "logo", "enabled": true }, + { "id": "workspaces", "enabled": true }, + { "id": "spacer", "enabled": true }, + { "id": "clock", "enabled": true }, + { "id": "spacer", "enabled": true }, + { "id": "statusIcons", "enabled": true }, + { "id": "tray", "enabled": true }, + { "id": "power", "enabled": true } + ] + } +} +NOSIGNAL_SHELL +# Flatten the Hyprland glassmorphism (blur/shadow off, solid windows, tighter +# corners + gaps). Sourced by caelestia's hyprland.conf right after variables.conf. +cat > "$M_HOME/.config/caelestia/hypr-vars.conf" <<'NOSIGNAL_HYPRVARS' +# NoSignal restraint — flatten Caelestia's glassmorphism (deleting restores stock). +# Default applications (caelestia's stock picks aren't installed: zen-browser/ +# codium/thunar — point the Super-key app binds at what NoSignal actually ships). +$terminal = kitty +$browser = chromium +$editor = gnome-text-editor +$fileExplorer = nautilus +$blurEnabled = false +$blurPopups = false +$blurInputMethods = false +$shadowEnabled = false +$windowOpacity = 1.0 +$windowRounding = 0 +$workspaceGaps = 10 +$windowGapsIn = 4 +$windowGapsOut = 8 +$singleWindowGapsOut = 12 +NOSIGNAL_HYPRVARS + +# --- Workspace Overview (standalone sidecar) ---------------------------------- +# quickshell-overview runs as its OWN qs instance next to caelestia, toggled by +# Super+Tab over IPC. Caelestia's shell is never modified (no fork/package). +echo "==> Installing workspace Overview (sidecar)..." +install -d -m 755 "$M_HOME/.config/quickshell" +tar -xzf "$NOSIGNAL_PAYLOAD/vendor/quickshell-overview.tar.gz" -C "$M_HOME/.config/quickshell" +# theme-match caelestia + low-RAM event previews + no blur +cat > "$M_HOME/.config/quickshell/overview/config.json" <<'NOSIGNAL_OV_CFG' +{ + "appearance": { "colorSource": "caelestia" }, + "overview": { "previewMode": "event", "previewsEnabled": true, "closeOnFocusLoss": true, "hideEmptyRows": true, "effects": { "enableBlur": false, "enableBackdrop": false, "glassMode": false } } +} +NOSIGNAL_OV_CFG + +# --- omarchy-send (LAN file transfer, LocalSend-compatible) ------------------- +# Vendored release binary; system-wide so every user gets it. The desktop entry +# opens the TUI in a floating kitty (TUI.float class — float rule above). +echo "==> Installing omarchy-send..." +install -m 755 "$NOSIGNAL_PAYLOAD/vendor/omarchy-send" /mnt/usr/local/bin/omarchy-send +install -d -m 755 /mnt/usr/share/applications /mnt/usr/share/icons/hicolor/scalable/apps +cat > /mnt/usr/share/icons/hicolor/scalable/apps/omarchy-send.svg <<'OMS_ICON' + + + + + +OMS_ICON +cat > /mnt/usr/share/applications/omarchy-send.desktop <<'OMS_DESKTOP' +[Desktop Entry] +Name=Omarchy-Send +Comment=Send & receive files over the LAN (LocalSend-compatible) +Exec=kitty --class TUI.float -e omarchy-send +Icon=omarchy-send +Terminal=false +Type=Application +Categories=Network;FileTransfer; +Keywords=localsend;share;transfer;airdrop; +OMS_DESKTOP + +# --- OS theme wallpapers ------------------------------------------------------- +# The user's own Moebius-style art set. caelestia's dynamic Material scheme is +# generated FROM the default wallpaper (foam-sea.png, change 18) in the scheme +# step below. +echo "==> Installing NoSignal wallpapers..." +install -d -m 755 "$M_HOME/Pictures/Wallpapers" +tar -xf "$NOSIGNAL_PAYLOAD/vendor/nosignal-wallpapers.tar" -C "$M_HOME/Pictures/Wallpapers" + +# Chromium on Wayland: pick the ozone backend automatically (native Wayland +# under Hyprland instead of XWayland). +cat > /mnt/etc/chromium-flags.conf <<'CHROMIUM_FLAGS' +--ozone-platform-hint=auto +CHROMIUM_FLAGS +# autostart the sidecar + toggle bind via caelestia's last-sourced user hook. +# Overview lives on Super+GRAVE (not Tab): the Omarchy keymap appended below +# puts next-workspace on Super+Tab — see dev-docs/BUILDER-HANDOFF.md. +cat > "$M_HOME/.config/caelestia/hypr-user.conf" <<'NOSIGNAL_HYPRUSER' +# NoSignal — overview sidecar autostart + keybind (deleting restores stock). +exec-once = qs -c overview -d +bind = Super, Grave, exec, qs ipc -c overview call overview toggle + +# F10: open Caelestia Settings (nexus). Super+Alt+Space is free (NoSignal moved +# the old float-toggle to Super+T). caelestia:nexus is the registered global +# shortcut that opens the Settings window. +bind = Super+Alt, Space, global, caelestia:nexus + +# hyprmoncfg writes monitor layout/resolution + workspace assignments here +# (the files are pre-created empty so Hyprland never warns about the source; +# hyprmoncfg refuses to write monitors.conf unless it is sourced). +source = ~/.config/hypr/monitors.conf +source = ~/.config/hypr/workspaces.conf + +# omarchy-send launches in a floating terminal window +windowrule = float true, match:class TUI\.float +windowrule = size 1100 700, match:class TUI\.float + +# Passwordless-sudo password prompt (Settings -> Services). Dedicated class + +# centered/pinned floating window; SudoToggleRow also dispatches `focuswindow` so +# the prompt grabs keyboard focus (without it the prompt missed keystrokes and +# three blank tries tripped pam_faillock — hardware bug 2026-06-20). +# NB: `stayfocused` is NOT valid in Hyprland 0.55's match: windowrule grammar +# (errors at parse time), so focus is forced from the shell via hyprctl instead. +windowrule = float on, match:class nosignal-sudo +windowrule = size 640 220, match:class nosignal-sudo +windowrule = center on, match:class nosignal-sudo +windowrule = pin on, match:class nosignal-sudo + +# polkit auth agent — none ran out of the box, so GUI privilege prompts (pkexec, +# nm-connection-editor system connections, etc.) silently failed. Non-fatal. +exec-once = systemctl --user start hyprpolkitagent.service +NOSIGNAL_HYPRUSER +# Keyboard layout from the installer prompt (appended unquoted — $XKB_LAYOUT). +{ + echo '' + echo '# Keyboard layout chosen at install' + echo "input:kb_layout = $XKB_LAYOUT" +} >> "$M_HOME/.config/caelestia/hypr-user.conf" + +# F1-B / F11-2: the hypr-user.conf override above was NOT winning — caelestia's +# stock hypr/hyprland/input.conf hardcodes `kb_layout = us` and is sourced such +# that it overrides the variable set above, so the Wayland session AND the lock +# surface came up US-layout on a GB system (mistyped password symbols were F1's +# original trigger). Set the chosen layout in that authoritative file too. The +# dotfiles are extracted once at install (not a package), so this is durable. +# ~/.config/hypr is a symlink into the clone; edit the real file under it. +INPUT_CONF="$M_HOME/.local/share/caelestia/hypr/hyprland/input.conf" +# Fall back to locating whichever file in the hypr tree carries kb_layout, in +# case upstream moves it (keeps the fix working across caelestia layout changes). +if [ ! -f "$INPUT_CONF" ]; then + INPUT_CONF=$(grep -rlE '^[[:space:]]*kb_layout[[:space:]]*=' \ + "$M_HOME/.local/share/caelestia/hypr/" 2>/dev/null | head -n1) +fi +if [ -n "$INPUT_CONF" ] && [ -f "$INPUT_CONF" ]; then + if grep -qE '^[[:space:]]*kb_layout[[:space:]]*=' "$INPUT_CONF"; then + sed -i "s|^\([[:space:]]*kb_layout[[:space:]]*=[[:space:]]*\).*|\1$XKB_LAYOUT|" "$INPUT_CONF" + else + printf '\n# Keyboard layout chosen at install (NoSignal)\ninput {\n kb_layout = %s\n}\n' "$XKB_LAYOUT" >> "$INPUT_CONF" + fi + echo " Hyprland kb_layout set to $XKB_LAYOUT in input.conf" +else + echo " NOTE: caelestia input.conf not found — relying on hypr-user.conf kb_layout only" +fi + +# Empty targets for hyprmoncfg (live in the dotfiles clone — ~/.config/hypr +# is a symlink into it; hyprmoncfg follows the symlink when saving). +touch "$M_HOME/.local/share/caelestia/hypr/monitors.conf" \ + "$M_HOME/.local/share/caelestia/hypr/workspaces.conf" + +# kitty: bash is the default shell (NoSignal layer change 2 — Omarchy setup); +# ~/.bashrc applies caelestia's colour-scheme escape sequences at terminal +# start (cat sequences.txt), so kitty gets themed the same way fish did. +install -d -m 755 "$M_HOME/.config/kitty" +cat > "$M_HOME/.config/kitty/kitty.conf" <<'NOSIGNAL_KITTY' +# NoSignal defaults — kitty is the default terminal ($terminal in hypr-vars.conf). +shell bash +font_family JetBrainsMono Nerd Font +font_size 12.0 +enable_audio_bell no +confirm_os_window_close 0 +NOSIGNAL_KITTY + +# fastfetch: the NoSignal layout (Hardware / Software / Age-Uptime boxes + the +# NoSignal logo). Shipped as a real config — NOT symlinked into the caelestia +# clone (fastfetch was dropped from the symlink loop above) so it overrides the +# dotfiles default. The logo lives beside it; config.jsonc points at it. +install -d -m 755 "$M_HOME/.config/fastfetch" +install -m 644 "$NOSIGNAL_PAYLOAD/vendor/fastfetch-config.jsonc" \ + "$M_HOME/.config/fastfetch/config.jsonc" +install -m 644 "$NOSIGNAL_PAYLOAD/vendor/fastfetch-logo.txt" \ + "$M_HOME/.config/fastfetch/logo.txt" + +# ---------------------------------------------- NoSignal layer (changes 1-26) -- +# The "os updates" rounds, baked in at install time: keybind cheatsheet, bash +# default shell, nosignal-update mechanism, yay+Shelly store, Omarchy default +# keybindings, omadots developer polish, hyprmoncfg, settings Updates page, +# system polish (1-9); universal copy/paste, screenshots, the launcher/ +# dashboard/monitor fix-ups, gaming enablement, SDDM + themed greeter (10-16, +# 18-19); Wi-Fi wrong-password recovery, monitor profile hot-load, yay-menu +# pre-answers, tidied cheatsheet, CLIAmp music player, Settings Additions +# page, uuctl hidden (20-26, the 2026-06-12 hardware-test round). Change 17 +# (deckshift-login) SHIPS in the tree but is NOT applied — gaming is opt-in +# post-install. The packages came from pacstrap; everything else comes from +# the layer tree itself, so the ISO and a live-updated box are identical. +# Conf fragments are appended FROM the extracted tree so they cannot drift. +nsi_phase "Applying the NoSignal layer" +echo "==> Installing NoSignal layer (os-updates round)..." +tar -xzf "$NOSIGNAL_PAYLOAD/vendor/nosignal-layer.tar.gz" -C "$M_HOME/.local/share" +LAYER="$M_HOME/.local/share/nosignal" +chmod +x "$LAYER/nosignal-update/bin/nosignal-update" "$LAYER/nosignal-update/migrations/"*.sh + +# On-box management skill: a fresh Claude Code on this machine auto-discovers +# the `nosignal` skill (its description is surfaced in the available-skills list) +# and can help the USER configure / fix / maintain their install — strictly +# end-user system management, NOT the dev/QA test protocol. The skill's +# references/ are copied from the layer's single-source docs at install time, so +# they track the build and can never drift (the staleness trap the frozen +# ONBOX-TEST-NOTES.md fell into). The originals stay in ~/.local/share/nosignal/. +SKILL_DIR="$M_HOME/.claude/skills/nosignal" +install -d -m 755 "$SKILL_DIR/references" +install -m 644 "$LAYER/onbox-skill/SKILL.md" "$SKILL_DIR/SKILL.md" +install -m 644 "$LAYER/ONBOX-AI-NOTES.md" "$SKILL_DIR/references/ONBOX-AI-NOTES.md" +install -m 644 "$LAYER/NoSignal-keybindings.md" "$SKILL_DIR/references/NoSignal-keybindings.md" + +# Commands onto ~/.local/bin (already on PATH via the profile drop above). +# nosignal-update stays a symlink INTO the layer tree — it locates its +# migrations by resolving its own path. +install -d -m 755 "$M_HOME/.local/bin" +install -m 755 "$LAYER/nosignal-keybinds" "$LAYER/nosignal-keybinds-gen" \ + "$LAYER/system-polish/nosignal-webapp-install" \ + "$LAYER/system-polish/nosignal-webapp-remove" \ + "$LAYER/system-polish/nosignal-welcome" \ + "$LAYER/updates-panel/nosignal-update-check" \ + "$LAYER/super-clipboard/super-copy" \ + "$LAYER/super-clipboard/super-paste" \ + "$LAYER/screenshots/nosignal-screenshot" \ + "$LAYER/additions-installer/nosignal-additions" \ + "$LAYER/app-theme-awareness/nosignal-app-theme-sync" \ + "$LAYER/kernel-reboot-notify/nosignal-reboot-check" \ + "$M_HOME/.local/bin/" +# Omarchy CLI shims (change 15) — let Omarchy-targeted gaming installers +# (DeckShift) run on NoSignal: omarchy-pkg-add -> yay, steam installer, NVIDIA +# GSP probes, restart-walker no-op. +install -m 755 "$LAYER/gaming-enablement/omarchy-pkg-add" \ + "$LAYER/gaming-enablement/omarchy-install-gaming-steam" \ + "$LAYER/gaming-enablement/omarchy-hw-nvidia-gsp" \ + "$LAYER/gaming-enablement/omarchy-hw-nvidia-without-gsp" \ + "$LAYER/gaming-enablement/omarchy-restart-walker" \ + "$M_HOME/.local/bin/" +ln -sf "$USER_HOME/.local/share/nosignal/nosignal-update/bin/nosignal-update" \ + "$M_HOME/.local/bin/nosignal-update" + +# --- change 27 (xdg-terminal-exec-handler, finding F2): app2unit hardcodes its +# terminal handler to `xdg-terminal-exec`, which NoSignal never shipped, so any +# Terminal=true desktop entry (and app2unit -T) failed with a critical +# notification. Ship a small shim on PATH that launches the configured terminal +# ($TERMINAL, else kitty), plus a system xdg-terminals.list pointing at kitty. +# (The shim self-removes if the real freedesktop package ever lands in a system +# bin — see the component installer.) +install -m 755 "$LAYER/xdg-terminal-exec-handler/xdg-terminal-exec" \ + "$M_HOME/.local/bin/xdg-terminal-exec" +install -d -m 755 /mnt/etc/xdg +printf 'kitty.desktop\n' > /mnt/etc/xdg/xdg-terminals.list +chmod 644 /mnt/etc/xdg/xdg-terminals.list + +# Mark every shipped migration as applied — the layer is baked in, so the +# user's first `nosignal-update` must report "0 new migrations" (they are +# idempotent, but re-running needs network/sudo and is noisy). Do NOT create +# the `welcomed` stamp: the first-login welcome notice must fire. +install -d -m 755 "$M_HOME/.local/state/nosignal" +for m in "$LAYER/nosignal-update/migrations/"*.sh; do + basename "$m" +done > "$M_HOME/.local/state/nosignal/applied" + +# --- change 29 (limine-uki-dead-entry, finding F4, BLOCKER): FIXED IN THE SEED. +# The dead-boot is now prevented at the source — the seeded limine.conf above +# ships a `protocol: efi` -> UKI entry as entry 1 with `default_entry: 1`, plus a +# `protocol: linux` fallback as entry 2. Limine preserves these manual entries + +# `default_entry` verbatim across every kernel-update regeneration, so the +# default boot target can never become the dead vmlinuz entry. No post-install +# conversion is needed (the old one-shot conversion mis-fired on 20260613). The +# limine-uki-dead-entry/ component is kept in the layer ONLY as a manual repair +# tool for boxes installed from a pre-fix ISO; it is NOT run at install anymore. + +# --- change 2: default shell bash, matching Omarchy. The fish PACKAGE stays +# (caelestia-meta hard-depends on it) and the hypr config keeps calling the +# *.fish script paths — replaced with bash ports under the same filenames. +echo "==> Default shell: bash (Omarchy setup)..." +install -d -m 755 "$M_HOME/.local/share/omarchy/default" +cp -a "$LAYER/fish-to-bash/bash" "$M_HOME/.local/share/omarchy/default/bash" +install -m 644 "$LAYER/fish-to-bash/bashrc" "$M_HOME/.bashrc" +install -m 755 "$LAYER/fish-to-bash/hypr-wsaction.bash" \ + "$M_HOME/.local/share/caelestia/hypr/scripts/wsaction.fish" +install -m 755 "$LAYER/fish-to-bash/hypr-configs.bash" \ + "$M_HOME/.local/share/caelestia/hypr/scripts/configs.fish" +# foot config lives in the dotfiles clone (~/.config/foot is a symlink) +sed -i 's/^shell=.*/shell=bash/' "$M_HOME/.local/share/caelestia/foot/foot.ini" +# Square corners everywhere (redesign goal): caelestia's rules.conf hardcodes +# `rounding 10` on Steam + some XWayland windows, ignoring $windowRounding=0. +# Zero them so those windows aren't the only rounded ones on the desktop. +sed -i -E 's/^(windowrule = )rounding 10,/\1rounding 0,/' \ + "$M_HOME/.local/share/caelestia/hypr/hyprland/rules.conf" 2>/dev/null || true + +# --- change 6: omadots developer polish. starship/btop replace the caelestia +# symlinks made above (re-linking restores stock); git behaviors arrive via +# include.path so user identity stays out of the image; LazyVim from vendor. +echo "==> omadots developer polish..." +rm -f "$M_HOME/.config/starship.toml" +install -m 644 "$LAYER/omadots-extras/starship.toml" "$M_HOME/.config/starship.toml" +rm -f "$M_HOME/.config/btop" +install -d -m 755 "$M_HOME/.config/btop" "$M_HOME/.config/tmux" "$M_HOME/.config/git" +install -m 644 "$LAYER/omadots-extras/btop.conf" "$M_HOME/.config/btop/btop.conf" +install -m 644 "$LAYER/omadots-extras/tmux.conf" "$M_HOME/.config/tmux/tmux.conf" +install -m 644 "$LAYER/omadots-extras/omadots.gitconfig" "$M_HOME/.config/git/omadots.gitconfig" +cat > "$M_HOME/.gitconfig" < Omarchy keymap + layer binds..." +{ + echo '' + cat "$LAYER/omarchy-keys/omarchy-keys-vars.conf" +} >> "$M_HOME/.config/caelestia/hypr-vars.conf" +{ + echo '' + cat "$LAYER/hyprland-keybinds-help.conf" + echo '' + cat "$LAYER/software-install/hyprland-software-install.conf" + echo '' + cat "$LAYER/omarchy-keys/omarchy-keys-user.conf" + echo '' + cat "$LAYER/monitor-control/hyprland-monitor-control.conf" + echo '' + sed -n '/# >>> super-clipboard/,/# <<< super-clipboard/p' \ + "$LAYER/super-clipboard/install-super-clipboard.sh" + echo '' + sed -n '/# >>> nosignal screenshots/,/# <<< nosignal screenshots/p' \ + "$LAYER/screenshots/install-screenshots.sh" + echo '' + # change 24: CLIAmp marked block (float rules + Super+M launch-or-focus) — + # shipped as a standalone conf fragment, appended VERBATIM markers included. + cat "$LAYER/cliamp-music/hyprland-cliamp.conf" + echo '' + echo '# NoSignal — one-time first-login welcome (stamps ~/.local/state/nosignal/welcomed)' + echo 'exec-once = ~/.local/bin/nosignal-welcome' +} >> "$M_HOME/.config/caelestia/hypr-user.conf" + +# --- change 11: screenshots land in ~/Pictures/Screenshots (clipboard too); +# swappy (the caelestia region/freeze annotate binds) saves to the same place. +install -d -m 755 "$M_HOME/Pictures/Screenshots" "$M_HOME/.config/swappy" +install -m 644 "$LAYER/screenshots/swappy-config" "$M_HOME/.config/swappy/config" + +# --- changes 7+8: user services. No systemd in the chroot — enable = the +# symlink systemctl would create, targets read from each unit's [Install]. +echo "==> Enabling user services (hyprmoncfgd, update check timer)..." +install -d -m 755 "$M_HOME/.config/systemd/user/default.target.wants" \ + "$M_HOME/.config/systemd/user/timers.target.wants" +install -m 644 "$LAYER/updates-panel/nosignal-update-check.service" \ + "$LAYER/updates-panel/nosignal-update-check.timer" \ + "$M_HOME/.config/systemd/user/" +ln -sf ../nosignal-update-check.timer \ + "$M_HOME/.config/systemd/user/timers.target.wants/nosignal-update-check.timer" +ln -sf /usr/lib/systemd/user/hyprmoncfgd.service \ + "$M_HOME/.config/systemd/user/default.target.wants/hyprmoncfgd.service" +# change 12 (fix for change 7): point hyprmoncfgd at hypr-user.conf (the file +# that actually sources monitors.conf — it refuses to write otherwise) and +# gate its start until Hyprland's IPC answers (avoids a login-race error +# notification). Same flags are baked into the Super+Ctrl+H bind above. +install -d -m 755 "$M_HOME/.config/systemd/user/hyprmoncfgd.service.d" +install -m 644 "$LAYER/monitor-control-fix/hyprmoncfgd-override.conf" \ + "$M_HOME/.config/systemd/user/hyprmoncfgd.service.d/override.conf" +# change 21 (monitor-hotload): path unit watches ~/.config/hyprmoncfg/profiles +# and try-restarts hyprmoncfgd, so a plain "Save Profile" in the Super+Ctrl+H +# TUI applies live. The watched dir must exist (empty — the no-pre-seeded- +# profiles rule from change 7 still stands). +install -m 644 "$LAYER/monitor-hotload/hyprmoncfgd-rescan.path" \ + "$LAYER/monitor-hotload/hyprmoncfgd-rescan.service" \ + "$M_HOME/.config/systemd/user/" +ln -sf ../hyprmoncfgd-rescan.path \ + "$M_HOME/.config/systemd/user/default.target.wants/hyprmoncfgd-rescan.path" +install -d -m 755 "$M_HOME/.config/hyprmoncfg/profiles" + +# --- change 30 (app-theme-awareness, finding F6): make external apps +# (Chrome/Electron/Firefox/GTK/Qt) follow Caelestia's light/dark mode. The sync +# script (installed to ~/.local/bin above) mirrors `caelestia scheme get` to the +# freedesktop appearance portal color-scheme + the GTK prefer-dark flag; a .path +# unit re-runs it whenever Caelestia's scheme.json changes, and .service runs it +# at login. portals.conf pins the Settings portal to gtk so the value is served. +echo "==> Enabling app-theme awareness (follow Caelestia dark/light)..." +install -m 644 "$LAYER/app-theme-awareness/nosignal-app-theme.service" \ + "$LAYER/app-theme-awareness/nosignal-app-theme.path" \ + "$M_HOME/.config/systemd/user/" +ln -sf ../nosignal-app-theme.path \ + "$M_HOME/.config/systemd/user/default.target.wants/nosignal-app-theme.path" +ln -sf ../nosignal-app-theme.service \ + "$M_HOME/.config/systemd/user/default.target.wants/nosignal-app-theme.service" +install -d -m 755 "$M_HOME/.config/xdg-desktop-portal" +install -m 644 "$LAYER/app-theme-awareness/portals.conf" \ + "$M_HOME/.config/xdg-desktop-portal/portals.conf" + +# --- changes 8/25/20: Updates page, Additions page, and Wi-Fi wrong-password +# recovery are now BAKED INTO the pinned nosignal-shell fork (Phase 2 — we own +# the shell, so the old patch-script + pacman-hook machinery that re-applied +# these after every caelestia-shell upgrade is gone). The Updates/Additions +# CLIs + units are still installed above; only the shell-side QML edits moved +# into the fork source. The Additions status cache is still seeded below. +install -d -m 755 /mnt/etc/pacman.d/hooks + +# --- change 31 (kernel-reboot-notify, finding F5): a pacman PostTransaction +# hook that prints a "reboot required" reminder (and "boot the UKI entry") in +# the pacman/yay output whenever a kernel image changes, so out-of-band +# `pacman -Syu`/`yay` updates warn too. The desktop notification + state stamp +# are handled by nosignal-reboot-check, which nosignal-update calls at its end. +echo "==> Installing kernel-reboot reminder pacman hook..." +install -m 644 "$LAYER/kernel-reboot-notify/95-nosignal-kernel-reboot.hook" \ + /mnt/etc/pacman.d/hooks/95-nosignal-kernel-reboot.hook + +# --- change 28 (caelestia-lock-faillock, finding F1, BLOCKER): the faillock-free +# lock-screen PAM service (assets/pam.d/caelestia) and the Pam.qml repoint +# (config: "passwd" -> "caelestia") are now BAKED INTO the pinned nosignal-shell +# fork (Phase 2). No patch script or pacman hook needed — the fork ships the +# correct lock auth, so a desktop screen-lock can never faillock the user out +# of their own session. + +# --- change 36 (sudo-timed-nopasswd): a Settings -> Services toggle (+ CLI) +# that grants the user NOPASSWD: ALL for 15 minutes then auto-reverts (a root +# systemd timer), with a boot-clean safety net and a sudoers `visudo -cf` +# validation before install. Installs the CLI + boot-clean service + the panel +# toggle; does NOT enable sudoless (that's a deliberate user action). The +# SudoToggleRow + its ServicesPage insert are now BAKED INTO the pinned +# nosignal-shell fork (Phase 2), so NOSIGNAL_SKIP_SHELL_PATCH tells the installer +# to skip the (now redundant) QML patch and no pacman hook is written. The CLI + +# boot-clean safety-net service still install here. +echo "==> Installing time-boxed passwordless-sudo toggle..." +arch-chroot /mnt env NOSIGNAL_SKIP_SHELL_PATCH=1 sh "$USER_HOME/.local/share/nosignal/sudo-timed-nopasswd/install-sudo-timed-nopasswd.sh" \ + || echo " (sudo-timed-nopasswd install failed — CLI/toggle may be absent)" + +# --- change 37 (cachyos-repo-switch): a Settings -> Services toggle (+ CLI) that +# switches the system to/from CachyOS optimized builds + the linux-cachyos kernel, +# passwordless and auto-detecting the best x86-64 tier (v4 > v3). Installs the CLI +# + the visudo-validated sudoers drop-in + the panel toggle; does NOT enable the +# CachyOS repos (a deliberate user action — heavy, networked, reboots the kernel). +# CachyRepoToggleRow + its ServicesPage insert are BAKED INTO the pinned +# nosignal-shell fork (Phase 2), so NOSIGNAL_SKIP_SHELL_PATCH skips the redundant +# QML patch. The CLI + sudoers install here. (gawk/curl/kitty/limine already in +# the base closure; the helper live-fetches the CachyOS keyring/mirrorlist/awk at +# enable time — enabling CachyOS is online by nature.) +echo "==> Installing CachyOS repo + kernel switch toggle..." +arch-chroot /mnt env NOSIGNAL_SKIP_SHELL_PATCH=1 sh "$USER_HOME/.local/share/nosignal/cachyos-repo-switch/install-cachyos-repo-switch.sh" \ + || echo " (cachyos-repo-switch install failed — CLI/toggle may be absent)" + +# --- change 4: Flathub remote for Shelly's flatpak pages. The vendored +# .flatpakrepo embeds the GPG key, so this is an offline-safe config write. +# Staged in /root, NOT /tmp: arch-chroot mounts a fresh tmpfs over the +# target's /tmp, which shadows anything copied there from outside. +echo "==> Configuring Flathub remote..." +cp "$NOSIGNAL_PAYLOAD/vendor/flathub.flatpakrepo" /mnt/root/flathub.flatpakrepo +arch-chroot /mnt flatpak remote-add --if-not-exists flathub /root/flathub.flatpakrepo \ + || echo " (flathub remote-add failed — once online run: flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo)" +rm -f /mnt/root/flathub.flatpakrepo + +# --- changes 9+26: hide launcher clutter via per-user overrides. The PACKAGES +# stay (avahi-daemon is needed for .local/printer discovery, foot is a +# caelestia-meta dep, uuctl ships with uwsm) — only their menu entries +# disappear. +echo "==> Hiding launcher clutter..." +install -d -m 755 "$M_HOME/.local/share/applications" +for id in avahi-discover bssh bvnc qv4l2 qvidcap foot footclient foot-server \ + org.gnupg.pinentry-qt org.gnupg.pinentry-qt5 uuctl; do + [ -f "/mnt/usr/share/applications/$id.desktop" ] || continue + cat > "$M_HOME/.local/share/applications/$id.desktop" <<'HIDDEN_ENTRY' +[Desktop Entry] +Type=Application +Name=Hidden by NoSignal +NoDisplay=true +Hidden=true +HIDDEN_ENTRY +done +# (change 9's printing piece — cups.socket — is already enabled in the base.) + +# --- hardware-test 2026-06-15 (finding F-T2): stop the redundant blueman tray +# applet from autostarting. The base ships /etc/xdg/autostart/blueman.desktop, +# but blueman-applet duplicates the redesign's own bar Bluetooth pill + BlueZ +# panel (and was the only thing keeping the now-gated system-tray pill visible). +# A per-user XDG autostart override (Hidden=true) suppresses just the applet; +# the blueman package stays so blueman-manager can still be launched by hand and +# Bluetooth control is unaffected. +echo "==> Disabling redundant blueman tray applet..." +install -d -m 755 "$M_HOME/.config/autostart" +cat > "$M_HOME/.config/autostart/blueman.desktop" <<'BLUEMAN_OFF' +[Desktop Entry] +Type=Application +Name=blueman-applet +Comment=NoSignal: redundant with the bar Bluetooth pill + BlueZ panel — disabled +Hidden=true +X-GNOME-Autostart-enabled=false +BLUEMAN_OFF +chmod 644 "$M_HOME/.config/autostart/blueman.desktop" + +# --- change 24: CLIAmp default music player. The package came from pacstrap +# ([omarchy] repo); the Super+M bind is the marked block appended above. Here: +# the desktop entry (launches in kitty, class cliamp) + the audio MIME +# defaults. Nothing else writes ~/.config/mimeapps.list at install time (the +# dotfiles ship none), so a plain write is safe. +echo "==> CLIAmp default music player..." +install -m 644 "$LAYER/cliamp-music/cliamp.desktop" \ + "$M_HOME/.local/share/applications/cliamp.desktop" +{ + echo '[Default Applications]' + for m in audio/mpeg audio/mp4 audio/x-m4a audio/aac audio/flac audio/x-flac \ + audio/ogg audio/x-vorbis+ogg audio/x-opus+ogg audio/wav audio/x-wav \ + audio/webm; do + echo "$m=cliamp.desktop" + done +} > "$M_HOME/.config/mimeapps.list" +chmod 644 "$M_HOME/.config/mimeapps.list" + +# --- change 19: SDDM greeter themed to match the desktop. Theme dir + sync +# script + [Theme] drop-in baked from the layer tree. The shipped theme.conf +# already carries the foam-sea Material palette (matching the change-18 +# default wallpaper), so the first-boot greeter matches the desktop before +# any sync has run; the background is seeded from the same wallpaper here. +# sddm-theme-sync needs a logged-in user to mirror, so it is NOT run in the +# chroot — after a wallpaper change the user runs `sudo sddm-theme-sync`. +echo "==> Installing SDDM greeter theme (caelestia)..." +install -d -m 755 /mnt/usr/share/sddm/themes/caelestia/backgrounds +install -m 644 "$LAYER/sddm-theme/caelestia/Main.qml" \ + /mnt/usr/share/sddm/themes/caelestia/Main.qml +install -m 644 "$LAYER/sddm-theme/caelestia/metadata.desktop" \ + /mnt/usr/share/sddm/themes/caelestia/metadata.desktop +install -m 644 "$LAYER/sddm-theme/caelestia/theme.conf" \ + /mnt/usr/share/sddm/themes/caelestia/theme.conf +install -m 644 "$M_HOME/Pictures/Wallpapers/nosignal/foam-sea.png" \ + /mnt/usr/share/sddm/themes/caelestia/backgrounds/wallpaper.png +install -m 755 "$LAYER/sddm-theme/sddm-theme-sync" /mnt/usr/local/bin/sddm-theme-sync +cat > /mnt/etc/sddm.conf.d/20-sddm-theme.conf <<'SDDMTHEME' +# Greeter theme matching the desktop scheme (sddm-theme component). +# Remove this file to fall back to SDDM's default greeter. +[Theme] +Current=caelestia +SDDMTHEME +chmod 644 /mnt/etc/sddm.conf.d/20-sddm-theme.conf + +arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "$USER_HOME" + +# --- colour scheme + wallpaper ------------------------------------------------- +# The OS theme: set the default Moebius wallpaper (foam-sea.png, change 18 — +# matches the baked SDDM greeter theme), then switch the scheme to +# `dynamic` so caelestia generates the Material palette FROM the wallpaper +# (whole shell + terminals pick it up). Both CLI calls are headless-safe (PIL + +# file writes, no compositor needed; verified the same way scheme-set was). +# Order matters: `wallpaper -f` first (writes the state + thumbnail), THEN +# `scheme set -n dynamic` (regenerates colours from that thumbnail). +# Fallbacks keep the old behaviour: shadotheme scheme, no wallpaper state. +echo "==> Seeding caelestia theme (foam-sea wallpaper + dynamic scheme)..." +as_user() { + arch-chroot /mnt runuser -u "$USERNAME" -- \ + env HOME="$USER_HOME" XDG_STATE_HOME="$USER_HOME/.local/state" \ + XDG_CONFIG_HOME="$USER_HOME/.config" XDG_CACHE_HOME="$USER_HOME/.cache" \ + "$@" +} +WALL="$USER_HOME/Pictures/Wallpapers/nosignal/foam-sea.png" +if as_user caelestia wallpaper -f "$WALL" >/dev/null 2>&1 \ + && as_user caelestia scheme set -n dynamic >/dev/null 2>&1 \ + && [ -f "$M_HOME/.local/state/caelestia/scheme.json" ]; then + echo " Dynamic scheme generated from $WALL" +else + echo " Dynamic scheme failed in chroot — falling back to shadotheme." + as_user caelestia scheme set -n shadotheme >/dev/null 2>&1 || true +fi +if [ ! -f "$M_HOME/.local/state/caelestia/scheme.json" ]; then + install -d -m 755 "$M_HOME/.local/state/caelestia" + install -m 644 "$NOSIGNAL_PAYLOAD/vendor/scheme-shadotheme.json" \ + "$M_HOME/.local/state/caelestia/scheme.json" +fi + +# --- change 25 (cont.): seed the Additions status cache so the page has data +# on first open (all 15 items not-installed on a fresh image — the page IS the +# opt-in). Needs as_user, hence here rather than next to the QML patch. +as_user "$USER_HOME/.local/bin/nosignal-additions" status >/dev/null 2>&1 \ + || echo " (Additions status seed failed — the page populates on first re-check)" + +arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "$USER_HOME/.local/state" 2>/dev/null || true + +# --------------------------------------------------------- install cleanup --- +# The file:// repo must not leak into the installed system: drop its synced db +# (the [nosignal] repo is not in the target's pacman.conf) and clear the package +# cache copies pacstrap made (~3.5GB — the install media still has them all). +nsi_phase "Finishing up" +echo "==> Cleaning install-time package cache..." +rm -f /mnt/var/lib/pacman/sync/nosignal.* +rm -f /mnt/var/cache/pacman/pkg/* + +# Heavy phase done — stop the spinner, restore on-screen output, show a centred +# completion screen. +nsi_spin_stop +trap - ERR +exec >/dev/tty 2>&1 +clear +tcecho "NoSignal install complete" "$NSI_GB" "$NSI_R" +printf '\n' >/dev/tty +tcecho "Remove the install media and reboot." "$NSI_B" "$NSI_R" +printf '\n' >/dev/tty +tcecho "First boot goes straight to the graphical login (SDDM, themed) —" "$NSI_DIM" "$NSI_R" +tcecho "log in and the themed Hyprland desktop starts. No internet needed;" "$NSI_DIM" "$NSI_R" +tcecho "run 'sudo pacman -Syu' once when you're online to sync databases." "$NSI_DIM" "$NSI_R" +printf '\n' >/dev/tty +PROMPT="Press ENTER to reboot now (Ctrl+C for a shell)... " +printf '%s' "$(nsi_pad "${#PROMPT}")" >/dev/tty +read -rp "$PROMPT" Building offline payload (cached in $OFFLINE)..." + mkdir -p "$OFFLINE/iso/repo" "$OFFLINE/iso/vendor" "$OFFLINE/aur" + + # ---- vendored source tarballs (pinned) ---------------------------------- + if [ ! -f "$OFFLINE/iso/vendor/caelestia-dotfiles.tar.gz" ]; then + echo "==> Vendoring caelestia dotfiles ($CAELESTIA_DOTS_COMMIT)..." + rm -rf "$OFFLINE/aur/caelestia" + git clone "$CAELESTIA_DOTS_REPO" "$OFFLINE/aur/caelestia" + git -C "$OFFLINE/aur/caelestia" checkout -q "$CAELESTIA_DOTS_COMMIT" + # Tarball root dir is 'caelestia' -> extracts to ~/.local/share/caelestia. + # .git is kept so the user can pull/diff upstream later. + tar -czf "$OFFLINE/iso/vendor/caelestia-dotfiles.tar.gz" -C "$OFFLINE/aur" caelestia + fi + if [ ! -f "$OFFLINE/iso/vendor/quickshell-overview.tar.gz" ]; then + echo "==> Vendoring quickshell-overview ($OVERVIEW_COMMIT)..." + rm -rf "$OFFLINE/aur/overview" + git clone "$OVERVIEW_REPO" "$OFFLINE/aur/overview" + git -C "$OFFLINE/aur/overview" checkout -q "$OVERVIEW_COMMIT" + # Tarball root dir is 'overview' -> extracts to ~/.config/quickshell/overview. + tar -czf "$OFFLINE/iso/vendor/quickshell-overview.tar.gz" -C "$OFFLINE/aur" overview + fi + # OS theme wallpapers — repo-local user art, re-tarred EVERY build (like the + # layer tree) so image swaps always reach the next ISO. PNGs don't compress, + # so plain tar. Stale Last Horizon tarballs from older builds are removed. + if [ ! -d "$WALLPAPERS_DIR" ] || [ ! -f "$WALLPAPERS_DIR/$DEFAULT_WALLPAPER" ]; then + echo "ERROR: wallpapers not found at $WALLPAPERS_DIR (need $DEFAULT_WALLPAPER)" >&2 + exit 1 + fi + echo "==> Vendoring OS theme wallpapers..." + rm -f "$OFFLINE/iso/vendor/lasthorizon-wallpapers.tar.gz" + # Tarball root dir is 'nosignal' -> extracts to ~/Pictures/Wallpapers/nosignal. + tar -cf "$OFFLINE/iso/vendor/nosignal-wallpapers.tar" \ + -C "$(dirname "$WALLPAPERS_DIR")" \ + --transform 's|^wallpapers|nosignal|' wallpapers + + # Plymouth boot-splash theme (NO SIGNAL logo on black) — repo-local, re-tarred + # EVERY build (like the wallpapers/layer) so logo/script edits reach the ISO. + # Tarball root dir is 'nosignal' -> extracts to + # /usr/share/plymouth/themes/nosignal. + if [ ! -f "$SCRIPT_DIR/assets/plymouth/nosignal/nosignal.script" ]; then + echo "ERROR: Plymouth theme not found at $SCRIPT_DIR/assets/plymouth/nosignal" >&2 + exit 1 + fi + echo "==> Vendoring Plymouth boot-splash theme..." + tar -czf "$OFFLINE/iso/vendor/nosignal-plymouth.tar.gz" \ + -C "$SCRIPT_DIR/assets/plymouth" nosignal + if [ ! -f "$OFFLINE/iso/vendor/lazyvim-starter.tar.gz" ]; then + echo "==> Vendoring LazyVim starter ($LAZYVIM_COMMIT)..." + rm -rf "$OFFLINE/aur/lazyvim-starter" + git clone "$LAZYVIM_REPO" "$OFFLINE/aur/lazyvim-starter" + git -C "$OFFLINE/aur/lazyvim-starter" checkout -q "$LAZYVIM_COMMIT" + rm -rf "$OFFLINE/aur/lazyvim-starter/.git" + # Tarball root dir is 'nvim' -> extracts to ~/.config/nvim. + tar -czf "$OFFLINE/iso/vendor/lazyvim-starter.tar.gz" -C "$OFFLINE/aur" \ + --transform 's|^lazyvim-starter|nvim|' lazyvim-starter + fi + if [ ! -f "$OFFLINE/iso/vendor/flathub.flatpakrepo" ]; then + echo "==> Vendoring Flathub remote definition..." + curl -fSL --proto '=https' --tlsv1.2 \ + -o "$OFFLINE/iso/vendor/flathub.flatpakrepo" "$FLATHUB_REPO_URL" + fi + # NoSignal layer tree — local source, small, and actively iterated on: re-tar + # it EVERY build (no cache stamp) so layer edits always reach the next ISO. + if [ ! -d "$NOSIGNAL_LAYER_DIR" ]; then + echo "ERROR: NoSignal layer tree not found at $NOSIGNAL_LAYER_DIR" >&2 + exit 1 + fi + echo "==> Vendoring NoSignal layer tree..." + # Tarball root dir is 'nosignal' -> extracts to ~/.local/share/nosignal. + tar -czf "$OFFLINE/iso/vendor/nosignal-layer.tar.gz" \ + -C "$(dirname "$NOSIGNAL_LAYER_DIR")" \ + --transform 's|^os updates|nosignal|' "os updates" + + if [ ! -f "$OFFLINE/iso/vendor/omarchy-send" ]; then + echo "==> Vendoring omarchy-send ($OMARCHY_SEND_VERSION)..." + curl -fSL --proto '=https' --tlsv1.2 -o "$OFFLINE/iso/vendor/omarchy-send" \ + "https://github.com/$OMARCHY_SEND_REPO/releases/download/$OMARCHY_SEND_VERSION/omarchy-send-linux-amd64" + chmod 755 "$OFFLINE/iso/vendor/omarchy-send" + fi + install -m 644 "$SCRIPT_DIR/assets/scheme-shadotheme.json" "$OFFLINE/iso/vendor/" + install -m 644 "$SCRIPT_DIR/assets/fastfetch-config.jsonc" \ + "$SCRIPT_DIR/assets/fastfetch-logo.txt" "$OFFLINE/iso/vendor/" + + # ---- bootstrap repo db (so the build chroot can reference it from day 0) - + if [ ! -e "$OFFLINE/iso/repo/nosignal.db" ]; then + tar -czf "$OFFLINE/iso/repo/nosignal.db.tar.gz" --files-from /dev/null + tar -czf "$OFFLINE/iso/repo/nosignal.files.tar.gz" --files-from /dev/null + ln -sf nosignal.db.tar.gz "$OFFLINE/iso/repo/nosignal.db" + ln -sf nosignal.files.tar.gz "$OFFLINE/iso/repo/nosignal.files" + fi + + # ---- build chroot -------------------------------------------------------- + # Clean-room AUR builds via devtools. The chroot's pacman.conf includes the + # local bootstrap repo: arch-nspawn auto-bind-mounts file:// Server dirs, so + # each built package becomes resolvable by the next build (caelestia-shell + # needs caelestia-cli + quickshell-git etc.). + cat > "$OFFLINE/chroot-pacman.conf" < Creating clean build chroot (devtools mkarchroot)..." + mkdir -p "$OFFLINE/chroot" + sudo mkarchroot -C "$OFFLINE/chroot-pacman.conf" "$OFFLINE/chroot/root" base-devel + fi + + # ---- AUR packages (clean-chroot builds, dependency order) ---------------- + local name p + for name in "${AUR_BUILD_ORDER[@]}"; do + if [ -f "$OFFLINE/aur/.built-$name" ]; then + echo " $name: cached." + continue + fi + echo "==> Building AUR package: $name..." + rm -rf "${OFFLINE:?}/aur/$name" + if [ "$name" = "nosignal-shell" ]; then + # NOT an AUR package — it's our fork of the caelestia shell. Pull the + # PKGBUILD from the fork at the pinned commit and pin the git source to + # the same commit (reproducible, exactly like quickshell-git below). The + # PKGBUILD already carries DISTRIBUTOR + the `git` makedepend that the + # shell's M3Shapes CMake FetchContent needs in a clean chroot, so no + # further patching is required here. + mkdir -p "$OFFLINE/aur/$name" + git clone --depth 1 "$NOSIGNAL_SHELL_REPO" -b nosignal "$OFFLINE/aur/$name.fork" + ( cd "$OFFLINE/aur/$name.fork" && git fetch --depth 1 origin "$NOSIGNAL_SHELL_COMMIT" && git checkout -q "$NOSIGNAL_SHELL_COMMIT" ) + cp "$OFFLINE/aur/$name.fork/packaging/PKGBUILD" "$OFFLINE/aur/$name/PKGBUILD" + rm -rf "$OFFLINE/aur/$name.fork" + sed -i 's|#branch=nosignal|#commit='"$NOSIGNAL_SHELL_COMMIT"'|' "$OFFLINE/aur/$name/PKGBUILD" + else + git clone --depth 1 "https://aur.archlinux.org/$name.git" "$OFFLINE/aur/$name" + fi + if [ "$name" = "quickshell-git" ]; then + # Pin to the validated commit (makepkg git source fragment). + sed -i 's|git+\$url\.git|&#commit='"$QUICKSHELL_GIT_COMMIT"'|' "$OFFLINE/aur/$name/PKGBUILD" + fi + # Self-heal the clean chroot's [nosignal] repo path: a project rename + # (hyprOS -> nosignal) leaves a stale Server path baked into the chroot's + # persistent pacman.conf, which only bites the first chroot build after the + # move (cached AUR pkgs skip the build). Keep it pointed at the live OFFLINE. + for pc in "$OFFLINE"/chroot/*/etc/pacman.conf; do + [ -f "$pc" ] && sudo sed -i "s#file:///.*/offline/iso/repo#file://$OFFLINE/iso/repo#g" "$pc" + done + # Refresh the ROOT chroot's sync dbs first: makechrootpkg -c clones the + # copy from root, and the dependency install inside runs withOUT -Sy — so + # without this, packages repo-added to [nosignal] after mkarchroot are + # invisible and chained builds fail with "target not found". + sudo arch-nspawn "$OFFLINE/chroot/root" pacman -Syu --noconfirm + ( cd "$OFFLINE/aur/$name" && sudo makechrootpkg -c -r "$OFFLINE/chroot" ) + for p in "$OFFLINE/aur/$name"/*.pkg.tar.zst; do + [[ "$(basename "$p")" == *-debug-* ]] && continue + cp -f "$p" "$OFFLINE/iso/repo/" + repo-add -R "$OFFLINE/iso/repo/nosignal.db.tar.gz" "$OFFLINE/iso/repo/$(basename "$p")" + done + touch "$OFFLINE/aur/.built-$name" + done + + # caelestia-meta builds from the PKGBUILD inside the dotfiles repo itself + # (the AUR snapshot lags it — the dotfiles clone is the authoritative + # source). Pure metapackage: no build() step, so plain makepkg -d on the + # host is safe (no dep pollution; pkgver() just needs git). + # PRIVACY: host makepkg records builddir/startdir in the package's + # .BUILDINFO — built from $HOME that would ship the builder's username on a + # distributable ISO. Build from a neutral /tmp copy instead. (Chroot-built + # packages don't have this problem: their paths are /build.) + if [ ! -f "$OFFLINE/aur/.built-caelestia-meta" ]; then + echo "==> Building caelestia-meta (from the pinned dotfiles clone)..." + if [ ! -d "$OFFLINE/aur/caelestia/.git" ]; then + rm -rf "$OFFLINE/aur/caelestia" + git clone "$CAELESTIA_DOTS_REPO" "$OFFLINE/aur/caelestia" + git -C "$OFFLINE/aur/caelestia" checkout -q "$CAELESTIA_DOTS_COMMIT" + fi + meta_tmp=$(mktemp -d /tmp/nosignal-meta.XXXXXX) + cp -a "$OFFLINE/aur/caelestia" "$meta_tmp/caelestia" # keep .git (pkgver) + ( cd "$meta_tmp/caelestia" && rm -f ./*.pkg.tar.zst \ + && BUILDDIR="$meta_tmp/build" makepkg -df --noconfirm ) + for p in "$meta_tmp/caelestia"/*.pkg.tar.zst; do + cp -f "$p" "$OFFLINE/iso/repo/" + repo-add -R "$OFFLINE/iso/repo/nosignal.db.tar.gz" "$OFFLINE/iso/repo/$(basename "$p")" + done + rm -rf "$meta_tmp" + touch "$OFFLINE/aur/.built-caelestia-meta" + fi + + # ---- full repo dependency closure ---------------------------------------- + # Resolve EVERYTHING the installer can possibly pacstrap — base set, every + # GPU variant, the limine tools (prebuilt, from the omarchy repo) and the + # caelestia stack (from the local repo, pulling its repo deps) — against an + # EMPTY local db, and download into the repo dir. Already-present files are + # skipped, so this is cheap on rebuilds. + echo "==> Downloading full package dependency closure..." + cat > "$OFFLINE/dl-pacman.conf" </dev/null 2>&1; then + paccache -rqk1 -c "$OFFLINE/iso/repo" || true + else + echo " (paccache not found — old package versions may bloat the ISO;" + echo " install pacman-contrib to enable pruning)" + fi + + # F5 (2026-06-12 hardware test): hyprland 0.55.3 segfaults at screen lock + # (renderer SEGV during lock/DPMS monitor churn; the watchdog then drops to + # the confusing safe-mode "basic desktop"). 0.55.4 backports the #15048 fix + # and was retested clean on the test box — refuse to ship anything older. + local hyprver + hyprver=$(basename "$(ls "$OFFLINE/iso/repo"/hyprland-[0-9]*.pkg.tar.zst | sort -V | tail -1)") + hyprver=${hyprver#hyprland-}; hyprver=${hyprver%-x86_64.pkg.tar.zst} + if [ "$(vercmp "$hyprver" 0.55.4)" -lt 0 ]; then + echo "ERROR: offline repo has hyprland $hyprver — F5 requires >= 0.55.4" >&2 + echo " (lock-screen renderer SEGV). Refresh mirrors and rebuild." >&2 + exit 1 + fi + echo " hyprland $hyprver (>= 0.55.4, F5 lock-screen fix) OK" + + echo "==> Building repo database..." + # Drop detached signatures (omarchy ships them): repo-add would embed them + # in the db and pacman then REQUIRES the signing key even under TrustAll. + # Offline install verifies nothing (SigLevel Never) — the sigs are dead weight. + rm -f "$OFFLINE/iso/repo"/*.sig + rm -f "$OFFLINE/iso/repo"/nosignal.db* "$OFFLINE/iso/repo"/nosignal.files* "$OFFLINE/iso/repo"/*.old + repo-add -q "$OFFLINE/iso/repo/nosignal.db.tar.gz" "$OFFLINE/iso/repo"/*.pkg.tar.zst + # ISO9660-safe: ship the .db/.files as REAL files, not symlinks. + local f + for f in db files; do + rm -f "$OFFLINE/iso/repo/nosignal.$f" + cp "$OFFLINE/iso/repo/nosignal.$f.tar.gz" "$OFFLINE/iso/repo/nosignal.$f" + done + + # ---- base package list (single source of truth for the installer) -------- + printf '%s\n' "${BASE_PKGS[@]}" > "$OFFLINE/iso/base-packages.list" + + echo "==> Offline payload ready: $(du -sh "$OFFLINE/iso" | cut -f1)" +} + +# =========================================================================== +# Host-side workflow: locate stock ISO, build offline payload, unsquash, +# inject, re-squash, repack, output. +# =========================================================================== + +# ---- locate stock ISO ---------------------------------------------------- +shopt -s nullglob +CANDIDATES=("$SCRIPT_DIR"/archlinux-*.iso) +shopt -u nullglob +STOCK_ISO="" +for iso in "${CANDIDATES[@]}"; do + [[ "$(basename "$iso")" == *NoSignal* ]] && continue + STOCK_ISO="$iso" + break +done + +if [ -z "$STOCK_ISO" ]; then + echo "ERROR: No stock Arch ISO found in $SCRIPT_DIR" >&2 + echo >&2 + echo "Download the latest from https://archlinux.org/download/ and put it" >&2 + echo "in this folder (filename must start with 'archlinux-')." >&2 + exit 1 +fi + +echo " Stock ISO: $(basename "$STOCK_ISO")" +echo " Output: $(basename "$OUT_ISO")" +echo + +# ---- tooling check ------------------------------------------------------- +for cmd in xorriso unsquashfs mksquashfs git sha512sum mkarchroot makechrootpkg repo-add makepkg; do + command -v "$cmd" >/dev/null || { + echo "ERROR: missing tool: $cmd" >&2 + echo " install with: sudo pacman -S libisoburn squashfs-tools git coreutils devtools pacman-contrib" >&2 + exit 1 + } +done + +# ---- OPTIONAL master SSH public key (OPT-IN ONLY) ------------------------- +# The ISO is distributable: by default NO key is baked and sshd stays disabled +# on installed systems — nothing identifying the build host ships on the +# image. For a personal/dev build (remote rescue over SSH), opt in explicitly: +# SSH_PUBKEY=/path/to/key.pub ./nosignal.sh +# (The old behaviour of auto-baking ~/.ssh/id_*.pub was removed deliberately — +# do not reintroduce it; a give-away ISO must never carry the builder's key.) +SSH_PUBKEY="${SSH_PUBKEY:-}" +HAVE_KEY=0 +if [ -z "$SSH_PUBKEY" ] || [ "$SSH_PUBKEY" = "none" ]; then + echo " SSH key: (none — sshd disabled on installs; distributable default." + echo " Opt in with SSH_PUBKEY=/path/to/key.pub for a dev build.)" +elif [ -f "$SSH_PUBKEY" ]; then + HAVE_KEY=1 + echo " SSH key: $SSH_PUBKEY (DEV BUILD — key baked in, sshd enabled on installs;" + echo " do NOT distribute this ISO)" +else + echo "ERROR: SSH_PUBKEY=$SSH_PUBKEY not found" >&2 + exit 1 +fi + +# ---- offline payload ------------------------------------------------------ +build_offline_payload + +# ---- workspace ----------------------------------------------------------- +sudo rm -rf "$WORK" +mkdir -p "$WORK" +SFS_DIR="$WORK/airootfs" + +# ---- pull squashfs out of the stock ISO --------------------------------- +echo "==> Extracting airootfs.sfs from stock ISO..." +xorriso -osirrox on -indev "$STOCK_ISO" \ + -extract /arch/x86_64/airootfs.sfs "$WORK/airootfs-stock.sfs" 2>&1 | tail -3 + +echo "==> Unsquashing airootfs (slow: ~1 min)..." +sudo unsquashfs -d "$SFS_DIR" "$WORK/airootfs-stock.sfs" >/dev/null + +# ---- inject payload ------------------------------------------------------ +if [ "$HAVE_KEY" = 1 ]; then + echo "==> Staging master SSH key for first-boot SSH access..." + sudo install -m 644 -o root -g root "$SSH_PUBKEY" "$SFS_DIR/root/master.pub" +fi + +echo "==> Injecting installer.sh..." +INSTALLER_TMP="$WORK/installer.sh" +write_installer "$INSTALLER_TMP" +sudo install -m 755 -o root -g root "$INSTALLER_TMP" "$SFS_DIR/root/installer.sh" + +echo "==> Masking online-only units (they hang/hijack the console offline)..." +# With no network, archlinux-keyring-wkd-sync pulls systemd-time-wait-sync, +# which waits for NTP sync FOREVER ("no limit") and repaints the console with +# its start-job spinner right over the running installer. Useless offline. +for unit in systemd-time-wait-sync.service archlinux-keyring-wkd-sync.service archlinux-keyring-wkd-sync.timer; do + sudo ln -sf /dev/null "$SFS_DIR/etc/systemd/system/$unit" +done + +echo "==> Wiring getty@tty1 to auto-launch the installer..." +sudo mkdir -p "$SFS_DIR/etc/systemd/system/getty@tty1.service.d" +sudo tee "$SFS_DIR/etc/systemd/system/getty@tty1.service.d/override.conf" >/dev/null <<'EOF' +[Service] +ExecStart= +ExecStart=-/usr/bin/bash /root/installer.sh +StandardInput=tty +StandardOutput=tty +Restart=no +RestartPreventExitStatus=1 2 3 4 5 6 7 8 +EOF + +# ---- extend USB enumeration timeout in boot loader cmdline -------------- +# Slow / fussy USB controllers (some Intel mini-PCs, NUCs) don't enumerate USB +# block devices within archiso's default search window. Adding rootdelay=60 to +# the kernel cmdline makes initramfs wait 60s before mounting root — fast +# hardware finds the device in 2s and moves on; slow hardware gets breathing +# room. consoleblank=0 keeps the live install console from DPMS-blanking. +echo "==> Patching bootloader cmdlines (rootdelay=60 for slow USB enumeration)..." +mkdir -p "$WORK/boot-edit" +BOOT_CFG_FILES=( + /loader/entries/01-archiso-linux.conf + /loader/entries/02-archiso-speech-linux.conf + /boot/syslinux/archiso_sys-linux.cfg +) +for path in "${BOOT_CFG_FILES[@]}"; do + dest="$WORK/boot-edit/$(basename "$path")" + # Don't pipe xorriso straight into tail — that masks its exit status under + # 'set -e', so a missing boot file would slip through here and instead blow + # up later at the unconditional -map during repack, with a murkier error. + if ! xorriso -osirrox on -indev "$STOCK_ISO" -extract "$path" "$dest" 2>"$WORK/xorriso-extract.log"; then + echo "ERROR: failed to extract $path from stock ISO:" >&2 + tail -3 "$WORK/xorriso-extract.log" >&2 + exit 1 + fi +done +for f in "$WORK/boot-edit"/*; do + [ -f "$f" ] || continue + sed -i -E '/archisosearchuuid=/ { /rootdelay=/! s/$/ rootdelay=60/ }' "$f" + sed -i -E '/archisosearchuuid=/ { /consoleblank=/! s/$/ consoleblank=0/ }' "$f" +done + +# ---- re-squash ----------------------------------------------------------- +echo "==> Re-squashing airootfs (slow: ~2 min)..." +sudo rm -f "$WORK/airootfs.sfs" +sudo mksquashfs "$SFS_DIR" "$WORK/airootfs.sfs" \ + -comp xz -Xbcj x86 -b 1M -noappend -no-progress -quiet + +# ---- regenerate sha512 -------------------------------------------------- +echo "==> Recalculating airootfs.sha512..." +( cd "$WORK" && sudo sha512sum airootfs.sfs | sudo tee airootfs.sha512 >/dev/null ) + +# ---- repack ISO --------------------------------------------------------- +# -boot_image any replay regenerates the hybrid MBR/GPT + El Torito layout +# SIZED TO THE NEW IMAGE. Never copy the stock system_area verbatim: its +# partition table describes the stock 1.2GB ISO, so everything past that +# (the /nosignal payload) lands OUTSIDE any partition and is unreadable when +# the media is mounted via a partition device (USB boot on real hardware). +echo "==> Repacking ISO (offline repo + replayed boot layout + volume UUID)..." +sudo rm -f "$OUT_ISO" + +# Force output volume UUID to match the stock's. archiso's initramfs hook +# searches for the boot media by this UUID (also baked into the kernel cmdline +# as archisosearchuuid=) — if xorriso regenerates the UUID with a fresh build +# timestamp, the search misses the USB entirely. Pinning the UUID keeps stock +# cmdline + marker file in sync. +STOCK_UUID=$(blkid -s UUID -o value "$STOCK_ISO") +STOCK_UUID_RAW=$(echo "$STOCK_UUID" | tr -d '-') + +xorriso \ + -indev "$STOCK_ISO" \ + -outdev "$OUT_ISO" \ + -volume_date "uuid" "$STOCK_UUID_RAW" \ + -boot_image any replay \ + -rm /arch/x86_64/airootfs.sfs -- \ + -map "$WORK/airootfs.sfs" /arch/x86_64/airootfs.sfs \ + -rm /arch/x86_64/airootfs.sha512 -- \ + -map "$WORK/airootfs.sha512" /arch/x86_64/airootfs.sha512 \ + -rm /loader/entries/01-archiso-linux.conf -- \ + -map "$WORK/boot-edit/01-archiso-linux.conf" /loader/entries/01-archiso-linux.conf \ + -rm /loader/entries/02-archiso-speech-linux.conf -- \ + -map "$WORK/boot-edit/02-archiso-speech-linux.conf" /loader/entries/02-archiso-speech-linux.conf \ + -rm /boot/syslinux/archiso_sys-linux.cfg -- \ + -map "$WORK/boot-edit/archiso_sys-linux.cfg" /boot/syslinux/archiso_sys-linux.cfg \ + -map "$OFFLINE/iso" /nosignal \ + -end 2>&1 | tail -5 + +# ---- cleanup ------------------------------------------------------------ +sudo chown "$INVOKING_USER:$INVOKING_GROUP" "$OUT_ISO" +sudo rm -rf "$WORK" + +echo +echo "==========================================================" +echo " Done: $OUT_ISO" +echo " Size: $(du -h "$OUT_ISO" | cut -f1)" +echo +echo " Fully offline installer — no network needed on the target." +echo +echo " Burn to USB:" +echo " sudo dd if='$OUT_ISO' of=/dev/sdX bs=4M status=progress conv=fsync" +echo +echo " Or drop $(basename "$OUT_ISO") into your Ventoy USB." +echo "==========================================================" diff --git a/os updates/NoSignal-keybindings.md b/os updates/NoSignal-keybindings.md new file mode 100644 index 0000000..2799934 --- /dev/null +++ b/os updates/NoSignal-keybindings.md @@ -0,0 +1,299 @@ +# NoSignal — Keybinding Map + +> **System:** Arch Linux (rolling), hostname `nosignal`, Wayland compositor **Hyprland**. +> **Base:** [Caelestia](https://github.com/caelestia-dots) dotfiles + shell (QuickShell). +> **NoSignal layer:** styling flattened, stock apps rebound, overview added, and +> **the key layout follows Omarchy's defaults** (see +> `omarchy-keys/README.md` for the full mapping and deviations). +> +> **Modifier legend:** `Super` = Windows/Meta key · `Ctrl` · `Alt` · `Shift`. +> `XF86*` = laptop media/function keys. `mouse_up/down` = scroll wheel. +> +> Bindings are **variable-driven**. The tables below show the *resolved* keys +> (variables from `variables.conf` + overrides applied). See "Source map" at the +> end for where each value comes from and how to change it. + +--- + +## 0. Built-in help (NoSignal addition) + +| Keys | Action | +|------|--------| +| `Super+K` | Open the searchable on-screen keybinding cheatsheet (Omarchy key) | +| `Super+/` | Same (alias) | +| `Super+F1` | Same (alias) | + +The cheatsheet is a fuzzel dmenu list — start typing to filter by key or by +category tag (`[Window]`, `[Apps]`, `[Audio]`, …). Selecting a line copies that +shortcut to the clipboard (if `wl-copy` is present); it never executes anything. + +**This very document is the single source of truth.** The on-screen help is +*generated* from the tables below — `nosignal-keybinds` regenerates the list from +this `.md` on every launch, so the cheatsheet can never drift from the doc. +Edit the keymap here and the help updates itself. + +## 1. Shell & session + +| Keys | Action | +|------|--------| +| `Super+Space` | Open app launcher (Omarchy key) | +| `Super` (tap & release) | Open app launcher (tapping another key/mouse cancels it) | +| `Super+Escape` | Session menu (logout / shutdown / reboot) | +| `Ctrl+Alt+Delete` | Session menu (alias, old key) | +| `Super+D` | Toggle dashboard — calendar + widgets (was hover-only) | +| `Super+Alt+Space` | Open Settings (Caelestia nexus) | +| `Super+N` | Toggle sidebar | +| `Super+Ctrl+K` | Show all shell panels | +| `Super+Ctrl+L` | Lock screen (Omarchy key) | +| `Super+Alt+L` | Restart shell daemon + re-lock (recover a broken lock) | +| `Super+Shift+Comma` | Clear all notifications (Omarchy key) | +| `Ctrl+Alt+C` | Clear all notifications (alias, old key) | +| `Ctrl+Super+Shift+R` | Kill the Caelestia shell (`qs -c caelestia kill`) | +| `Ctrl+Super+Alt+R` | Restart the Caelestia shell | +| `Super+Alt+F12` | Fire a test notification (dev/debug) | + +## 2. Overview (NoSignal addition) + +| Keys | Action | +|------|--------| +| `Super+Grave` | Toggle the overview sidecar (moved off Super+Tab for Omarchy) | + +## 3. Workspaces + +| Keys | Action | +|------|--------| +| `Super+1`…`9`,`0` | Go to workspace 1–10 | +| `Super+Tab` | Next workspace (Omarchy key) | +| `Super+Shift+Tab` | Previous workspace (Omarchy key) | +| `Super+Ctrl+Tab` | Back to former workspace (Omarchy key) | +| `Ctrl+Super+1`…`9`,`0` | Go to workspace **group** 1–10 | +| `Ctrl+Super+Left` / `Right` | Previous / next workspace | +| `Super+Page_Up` / `Page_Down` | Previous / next workspace | +| `Super` + scroll up/down | Next / previous workspace | +| `Ctrl+Super` + scroll up/down | Jump workspace group ±10 | +| `Super+S` | Toggle special (scratchpad) workspace | + +## 4. Moving windows between workspaces + +| Keys | Action | +|------|--------| +| `Super+Shift+1`…`9`,`0` | Move window to workspace 1–10 (Omarchy key) | +| `Super+Alt+1`…`9`,`0` | Move window to workspace 1–10 (alias, old key) | +| `Ctrl+Super+Alt+1`…`9`,`0` | Move window to workspace **group** 1–10 | +| `Super+Alt+Page_Up` / `Page_Down` | Move window to workspace ±1 | +| `Super+Alt` + scroll up/down | Move window to workspace ±1 | +| `Ctrl+Super+Shift+Right` / `Left` | Move window to workspace +1 / −1 | +| `Ctrl+Super+Shift+Up` | Move window to special workspace | +| `Ctrl+Super+Shift+Down` | Move window to first empty workspace | +| `Super+Alt+S` | Move window to special workspace | + +## 5. Window focus, move & resize + +| Keys | Action | +|------|--------| +| `Super+←/→/↑/↓` | Move focus left/right/up/down | +| `Alt+Tab` / `Shift+Alt+Tab` | Cycle focus next / previous window (Omarchy key) | +| `Super+Shift+←/→/↑/↓` | Move window in that direction | +| `Super+Minus` / `Super+Equal` | Resize narrower / wider (−/+10% width) | +| `Super+Shift+Minus` / `Equal` | Resize shorter / taller (−/+10% height) | +| `Super+Alt+←/→/↑/↓` | Resize active window | +| `Super` + left-drag | Move window (mouse) | +| `Super+Z` + drag | Move window (mouse) | +| `Super` + right-drag | Resize window (mouse) | +| `Super+X` + drag | Resize window (mouse) | +| `Ctrl+Super+\` | Center window | +| `Ctrl+Super+Alt+\` | Resize to 55%×70% and center | +| `Super+Alt+\` | Picture-in-picture (PiP) mode | +| `Super+T` | Toggle floating (Omarchy key) | +| `Super+J` | Toggle window split (Omarchy key) | +| `Super+P` | Pseudo window (Omarchy key) | +| `Super+O` | Pin window — show on all workspaces (Omarchy "pop out") | +| `Super+F` | Fullscreen | +| `Super+Alt+F` | Fullscreen **with borders** | +| `Super+W` | Close window (Omarchy key) | + +## 6. Window groups (tabbed stacks) + +| Keys | Action | +|------|--------| +| `Super+G` | Toggle group on active window (Omarchy key) | +| `Super+Alt+G` | Move window out of group (Omarchy key) | +| `Super+Alt+Tab` | Cycle to next window in group (Omarchy key) | +| `Super+Alt+Shift+Tab` | Cycle to previous window in group (Omarchy key) | +| `Ctrl+Alt+Tab` | Change active group member (forward) | +| `Ctrl+Shift+Alt+Tab` | Change active group member (backward) | + +## 7. Apps + +| Keys | Action | App (NoSignal default) | +|------|--------|----------------------| +| `Super+Return` | Terminal | **kitty** | +| `Super+Shift+Return` | Browser | **chromium** | +| `Super+Shift+B` | Browser (alias) | **chromium** | +| `Super+Shift+N` | Text editor | **gnome-text-editor** | +| `Super+Shift+F` | File manager | **nautilus** | +| `Super+Alt+E` | File manager (alt) | nemo | +| `Super+Shift+G` | GitHub Desktop | github-desktop | +| `Super+I` | Install software (app store) | **Shelly** | +| `Ctrl+Alt+Escape` | Process/task viewer | qps | +| `Super+Ctrl+A` | Audio control (Omarchy key) | pavucontrol | +| `Ctrl+Alt+V` | Audio control (alias) | pavucontrol | +| `Super+Ctrl+H` | Monitor layout editor & profiles | **hyprmoncfg** (TUI) | + +> All app binds use Omarchy's keys. Caelestia's stock app picks +> (foot / zen-browser / codium / thunar) are **not installed**; NoSignal points +> these binds at shipped apps via `hypr-vars.conf`. + +## 8. Special-workspace toggles + +| Keys | Action | +|------|--------| +| `Super+Ctrl+T` | System monitor — "Activity" (Omarchy key) | +| `Ctrl+Shift+Escape` | System monitor (alias) | +| `Super+Shift+M` | Music (Omarchy key) | +| `Super+M` | Music player — CLIAmp, floating (default for audio files) | +| `Super+Shift+D` | Communication (moved from Super+D — that's the dashboard now) | +| `Super+R` | To-do | + +## 9. Screenshots & screen recording + +| Keys | Action | +|------|--------| +| `Print` | Region capture (crosshair) → clipboard + ~/Pictures/Screenshots | +| `Shift+Print` | Full screen → clipboard + ~/Pictures/Screenshots | +| `Super+Shift+S` | Region capture, frozen screen (swappy → ~/Pictures/Screenshots) — becomes **Gaming Mode** after opting in (see §14) | +| `Super+Shift+Alt+S` | Region capture, live (swappy → ~/Pictures/Screenshots) | +| `Super+Print` | Color picker — hyprpicker (Omarchy key) | +| `Super+Shift+C` | Color picker (alias) | +| `Ctrl+Alt+R` | Record screen (no audio) | +| `Super+Alt+R` | Record screen **with sound** | +| `Super+Shift+Alt+R` | Record a region | + +## 10. Audio & brightness + +| Keys | Action | +|------|--------| +| `XF86AudioRaiseVolume` / `LowerVolume` | Volume ±10% (unmutes first) | +| `XF86AudioMute` | Mute / unmute output | +| `XF86AudioMicMute` | Mute / unmute microphone | +| `XF86MonBrightnessUp` / `Down` | Screen brightness up / down | + +## 11. Media playback + +| Keys | Action | +|------|--------| +| `Ctrl+Super+Space` or `XF86AudioPlay`/`Pause` | Play / pause | +| `Ctrl+Super+Equal` or `XF86AudioNext` | Next track | +| `Ctrl+Super+Minus` or `XF86AudioPrev` | Previous track | +| `XF86AudioStop` | Stop | + +## 12. Clipboard & emoji + +| Keys | Action | +|------|--------| +| `Super+C` | **Copy** — universal (terminals → Ctrl+Shift+C, GUI → Ctrl+C) | +| `Super+V` | **Paste** — universal (terminals → Ctrl+Shift+V, GUI → Ctrl+V) | +| `Super+Ctrl+V` | Clipboard history (Omarchy key) | +| `Super+Alt+V` | Clipboard history — delete-entry mode | +| `Super+Ctrl+E` | Emoji picker (Omarchy key) | +| `Super+Period` | Emoji picker (alias) | +| `Ctrl+Shift+Alt+V` | Alternate paste — types last clip (needs ydotool, NOT installed: dead bind) | + +## 13. Power + +| Keys | Action | +|------|--------| +| `Super+Shift+L` | Suspend-then-hibernate | + +## 14. Gaming Mode (opt-in) + +Not active on a fresh install. Opt in on the installed system with +`sh ~/deckshift/deckshift.sh`, then +`sh ~/.local/share/nosignal/deckshift-login/install-deckshift-login.sh`. + +`Super+Shift+S` is bound in the **base** and self-guards: it launches Gaming Mode +only if DeckShift is installed and **does nothing** otherwise. (It replaces +caelestia's Super+Shift+S screenshot-freeze for everyone — region shots stay on +Print / Shift+Print / Super+Shift+Alt+S.) + +| Keys | Action | +|------|--------| +| `Super+Shift+S` | Switch to Gaming Mode (full DeckShift gamescope/Steam session via SDDM restart) **if DeckShift is installed**; does nothing otherwise | +| `Super+Shift+R` | Exit Gaming Mode back to the desktop — works **inside the gaming session only** (or Steam > Power > Exit to Desktop) | + +Cold boots always show the SDDM password greeter; only the desktop↔gaming +switch itself skips the password (one-shot autologin). + +--- + +## Source map (for the install-system handoff) + +All paths below; `~/.config/hypr` → symlink → `~/.local/share/caelestia/hypr`. + +| File | Role | +|------|------| +| `~/.config/hypr/hyprland.conf` | Top-level; sources everything in order | +| `~/.config/hypr/hyprland/keybinds.conf` | **The bind definitions** (uses `$kb*` variables) | +| `~/.config/hypr/variables.conf` | **Default key + app values** (`$kbCloseWindow = Super, Q`, etc.) | +| `~/.config/caelestia/hypr-vars.conf` | **NoSignal overrides** — app rebinds + flattened styling + **Omarchy `$kb*` remaps** | +| `~/.config/caelestia/hypr-user.conf` | **NoSignal overrides** — overview (Super+Grave), help binds, **Omarchy unbinds/extra binds**, `kb_layout = gb`, window rules | +| `~/.local/bin/nosignal-keybinds-gen` | **Generator** — parses this `.md` → display-ready cheatsheet lines (single source of truth) | +| `~/.local/bin/nosignal-keybinds` | **NoSignal help launcher** — regenerates from this doc each run, shows the fuzzel dmenu | +| `~/.local/share/nosignal/keybinds.list` | Generated cache (auto-rebuilt; used as fallback if the doc is missing) | + +**How the override layering works** (load order matters — later wins): +`variables.conf` sets stock defaults → `hyprland.conf` sources `hypr-vars.conf` +*before* `keybinds.conf` (so `$kb*` remaps take effect) and `hypr-user.conf` +*after everything* (so its `unbind`/`bind` lines win). + +**NoSignal deltas vs. stock Caelestia:** +- **Key layout remapped to Omarchy defaults** — full mapping table + and deliberate deviations in `omarchy-keys/README.md`. +- Apps: terminal `foot→kitty`, browser `zen-browser→chromium`, + editor `codium→gnome-text-editor`, file manager `thunar→nautilus`. +- Added bind: `Super+Grave` → overview sidecar (`exec-once = qs -c overview -d`). +- Added bind: `Super+K` (+ `Super+/`, `Super+F1`) → searchable keybinding + cheatsheet, **generated from this document** so the two never drift. +- Added bind: `Super+I` → Shelly software store (repos + AUR + Flathub); yay + and flatpak/Flathub added underneath (see `software-install/`). +- Added binds: `Super+C` / `Super+V` universal copy/paste (clipboard + history → `Super+Ctrl+V`); `Print` region screenshot; + `Super+D` dashboard (communication → `Super+Shift+D`). +- Keyboard layout forced to `gb` (UK). +- Glassmorphism off: blur/shadows disabled, opacity 1.0, smaller rounding/gaps. +- Keys left unbound by the Omarchy remap: `Super+Q`, `Super+E`, + `Super+U`, `Super+Comma`, `Super+L`, `Super+Alt+Space`. + +### Rebuild: this whole component ships in Downloads + +Because the live system gets wiped and only Downloads is kept, the entire +feature is packaged there and reinstalled with one command per component: + +| File in Downloads | Installs to | +|-------------------|-------------| +| `nosignal-keybinds-gen` | `~/.local/bin/` (the markdown→list generator) | +| `nosignal-keybinds` | `~/.local/bin/` (the cheatsheet launcher) | +| `NoSignal-keybindings.md` (this file) | `~/.local/share/nosignal/` (canonical source of truth) | +| `hyprland-keybinds-help.conf` | appended to `~/.config/caelestia/hypr-user.conf` (Super+/ + F1 binds) | +| `install-keybinds-help.sh` | the installer — run it after rebuild | +| `omarchy-keys/` | the Omarchy remap (vars + user conf + installer) | + +```sh +sh ~/Downloads/install-keybinds-help.sh +sh ~/Downloads/omarchy-keys/install-omarchy-keys.sh +``` + +Installers are idempotent (safe to re-run) and reload Hyprland if it's +running. **No PATH dependency:** binds use the absolute path +`~/.local/bin/nosignal-keybinds`, and that script locates its generator next to +itself — so it works even if `~/.local/bin` isn't on the session `PATH`. +No keymap data lives in the scripts; editing this doc is all that's needed to +change the on-screen help. Deps: `awk`, `fuzzel`, optional `wl-copy`. + +**To change a binding:** edit `keybinds.conf` for new actions, or redefine the +matching `$kb*` variable. Put NoSignal-specific changes in `hypr-user.conf` / +`hypr-vars.conf` so they survive a Caelestia update (the `hypr` symlink target +gets overwritten on upgrade; the two override files do not). + +--- +*Keybindings follow Omarchy defaults; the Super+K cheatsheet is generated from this file.* diff --git a/os updates/ONBOX-AI-NOTES.md b/os updates/ONBOX-AI-NOTES.md new file mode 100644 index 0000000..e22f86d --- /dev/null +++ b/os updates/ONBOX-AI-NOTES.md @@ -0,0 +1,128 @@ +# NoSignal — notes for an AI agent working on this machine + +You are on **NoSignal**: Arch Linux + Hyprland + the Caelestia shell (Quickshell) ++ SDDM (themed greeter). A "NoSignal layer" of config and tooling sits ON TOP of +stock Caelestia. Read this before changing desktop/system config — your job is to +help the user manage their machine safely. + +## Config layout — what is safe to edit + +- `~/.config/caelestia/` — **the NoSignal override layer** (edit here): + - `hypr-user.conf` — user binds/rules/exec-once; sourced LAST, so it wins. + This is the right place for new keybinds. `unbind =` works here. + - `hypr-vars.conf` — `$variables` ($terminal, $kb*, gaps, blur…); sourced + BEFORE keybinds.conf consumes them. + - `shell.json`, `shell-tokens.json` — Caelestia shell appearance/behaviour. + - Deleting any of these files restores stock Caelestia behaviour. +- `~/.config/hypr` is a **SYMLINK** into `~/.local/share/caelestia` (the + dotfiles clone). Several other `~/.config` entries (foot, fish, fastfetch, + uwsm) are symlinks into it too. **Never replace these symlinks with copies** + and never edit the clone's hyprland.conf directly — use hypr-user.conf. +- `~/.config/hypr/scripts/wsaction.fish` and `configs.fish` are **bash** ports + kept under their original .fish names (the stock config calls those paths). + Keep the names and the bash shebang. + +## Updates + +- `~/.local/share/nosignal/` — the on-system layer: component sources, the + keybindings doc, and `nosignal-update/` (the update command + its migrations). +- **`nosignal-update`** = the system update command: snapper snapshot → + `yay -Syu` → run any NEW migrations. Applied migrations are tracked in + `~/.local/state/nosignal/applied` — do not re-run or edit by hand. + +## Keybindings + +- `Super+K` (aliases `Super+/`, `Super+F1`) = searchable fuzzel cheatsheet. +- **Source of truth: `~/.local/share/nosignal/NoSignal-keybindings.md`** — the + cheatsheet is GENERATED from it (`nosignal-keybinds-gen`). If you add a bind + in hypr-user.conf, document it in that file too. +- Layout follows **Omarchy defaults**: Super+Return terminal (kitty, bash), + Super+W close, Super+Space launcher, Super+Tab next workspace, + **Super+Grave = workspace overview**, Super+I Shelly store, + Super+Ctrl+H monitor editor, Super+Shift+number move-to-workspace + (bound by KEYCODE — GB layout). +- **Super+C / Super+V = universal copy/paste** (class-aware sendshortcut; + clipboard HISTORY is Super+Ctrl+V). **Print = region screenshot** → + clipboard + ~/Pictures/Screenshots (Shift+Print full). **Super+D = + dashboard/calendar** (communication workspace → Super+Shift+D). + **Super+M = CLIAmp**, the default music player (floating TUI; audio files + open in it via `~/.config/mimeapps.list`). The shell's music/MPRIS panel + is **Super+Shift+M**. + +## Shell / desktop architecture gotchas + +- The Caelestia shell runs from `/etc/xdg/quickshell/caelestia`. NoSignal + patches THREE things there, each re-applied by its own pacman hook after + every caelestia-shell upgrade: the Settings → System → **Updates** page + (`PageCompRegistry.qml` + `pages/UpdatesPage.qml`), the Settings → + **Additions** page (same registry + `pages/AdditionsPage.qml` + + Plugins→Additions relabel in `PageRegistry.qml`; backend = + `nosignal-additions` + `additions.json` in the layer tree), and the Wi-Fi + wrong-password recovery in `utils/NetworkConnection.qml`. + **Do NOT create `~/.config/quickshell/caelestia`** — a user copy shadows + /etc/xdg and silently drops the patches (and future ones). + If the shell ever fails at login (background but no bar): reproduce with + `qs -c caelestia -n` from a TTY (`WAYLAND_DISPLAY=wayland-1`) to get the QML + error chain; suspect the page patches first. +- The workspace **overview is a separate Quickshell instance** + (`~/.config/quickshell/overview`, `qs -c overview -d`, IPC-toggled by + Super+Grave). It is independent of the main shell. +- The update-status timer (`nosignal-update-check.timer`, user), + `hyprmoncfgd` (monitor hotplug profiles, user) and + `hyprmoncfgd-rescan.path` (watches `~/.config/hyprmoncfg/profiles/` and + bounces the daemon so plain profile saves apply live) are enabled systemd + USER units. Status: `systemctl --user status `. + +## Monitors + +- Use **hyprmoncfg** (`Super+Ctrl+H`, TUI) — it writes + `~/.config/hypr/monitors.conf` (+ workspaces.conf), which hypr-user.conf + sources. Keep those `source =` lines; hyprmoncfg refuses to write otherwise. +- Both the TUI bind and the `hyprmoncfgd` daemon run with + `--hypr-config ~/.config/caelestia/hypr-user.conf --monitors-conf + ~/.config/hypr/monitors.conf` (hyprmoncfg doesn't follow nested `source =` + includes). Keep the flags; the daemon's drop-in lives at + `~/.config/systemd/user/hyprmoncfgd.service.d/override.conf`. + +## Login / display manager / gaming + +- **SDDM** is the display manager. Config drop-ins in `/etc/sddm.conf.d/`: + `10-nosignal.conf` (X11 greeter, uwsm session notes) + `20-sddm-theme.conf` + (themed greeter). The desktop session MUST stay `hyprland-uwsm.desktop` + (uwsm-managed) — plain hyprland.desktop loses caelestia's session env. +- The greeter theme (`/usr/share/sddm/themes/caelestia`) mirrors the desktop + scheme. After changing wallpaper, run **`sudo sddm-theme-sync`** to update + the login screen (it does not auto-follow). +- **Gaming Mode is OPT-IN**: run `sh ~/deckshift/deckshift.sh`, then + `sh ~/.local/share/nosignal/deckshift-login/install-deckshift-login.sh` + (re-run the fix after any deckshift.sh re-run). Then Super+Shift+S enters + the full gamescope/Steam session; Super+Shift+R (inside it) returns. Cold + boots always show the password greeter (one-shot autologin). `[multilib]` + is enabled and `omarchy-pkg-add` & friends are shims in `~/.local/bin`. + +## Packages / system + +- `yay` for AUR; **Shelly** (`shelly-ui`, Super+I) is the GUI store + (repos + AUR + Flathub). Flathub remote is preconfigured. Settings → + **Additions** installs curated optional software (DeckShift, Spotify, + Once, Obsidian, OBS, Claude Code, Codex, opencode) from official sources + only — extend via `additions.json` in the layer tree, no code changes. +- The firewall is **ufw**, enabled with Omarchy-style defaults — it is easy + to miss because nothing advertises it: `sudo ufw status`. +- The `fish` package is REQUIRED by caelestia-meta — do not remove it, even + though bash is the login/terminal shell. +- Btrfs + snapper: every pacman transaction snapshots (snap-pac); snapshots + are BOOTABLE from the Limine menu (limine-snapper-sync, UKI at + /boot/EFI/Linux/nosignal_linux.efi). Worst case: reboot → pick a snapshot. +- `[omarchy]` repo in pacman.conf provides prebuilt limine-snapper tools. +- omarchy-send is installed (LAN file transfer; receive dir `~/Omarchy-Send`, + TUI in launcher, CLI: `omarchy-send -to `). Its config + contains a private key — never copy/share `~/.config/omarchy-send/`. + +## Theme + +- Wallpapers: `~/Pictures/Wallpapers/nosignal/` (the OS art set; default + foam-sea.png). The colour scheme is Caelestia's **dynamic** Material scheme, + generated FROM the current wallpaper — change wallpaper via the Caelestia + UI or `caelestia wallpaper -f ` and the palette follows. The SDDM + greeter does NOT auto-follow: run `sudo sddm-theme-sync` after. diff --git a/os updates/additions-extra/README.md b/os updates/additions-extra/README.md new file mode 100755 index 0000000..28f9e3b --- /dev/null +++ b/os updates/additions-extra/README.md @@ -0,0 +1,34 @@ +# additions-extra (Settings → Additions: 7 new items) + +Merges seven items into `additions.json` (8 → 15 items). + +Adds seven curated optional apps to Settings → Additions. All follow the +manifest policy (**official repos `[extra]`/`[omarchy]`, or official upstream +installers — no AUR, no Flatpak**) and are **GPU-agnostic** in wording and +behaviour (so they're correct on AMD boxes too). + +| id | name | source | notes | +|----|------|--------|-------| +| `ollama` | Ollama | `[extra]` | install picks `ollama-cuda` (NVIDIA) / `ollama-rocm` (AMD) / `ollama` (CPU) by detecting the GPU vendor via `lspci` — stays agnostic | +| `lmstudio` | LM Studio | `[omarchy]` | `lmstudio-bin` (Provides/Conflicts `lmstudio`) | +| `dropbox` | Dropbox | `[omarchy]` | daemon + cli + nautilus-dropbox; enables the user service; links account on first start | +| `tailscale` | Tailscale | `[extra]` | installs + enables `tailscaled`; user runs `sudo tailscale up` | +| `pinta` | Pinta | `[omarchy]` | Paint.NET-style image editor | +| `kdenlive` | Kdenlive | `[extra]` | video editor; hardware-accelerated export via ffmpeg | +| `pi` | Pi Coding Agent | `curl pi.dev/install.sh` | AI coding-agent CLI, same family as claude-code/codex/opencode | + +## Files +- `additions.json` — the **full merged manifest** (15 items) for the builder to + ship as the canonical Settings→Additions manifest. +- `merge-additions.sh` — idempotent merge of the 7 new items (by id) into an + already-installed system's manifest (skips ones already present). No root. +- `migrations/1781449200-additions-extra.sh` — delegates to it. + +## Builder integration +Replace the layer's `additions-installer/additions.json` with the bundled +15-item file (or cherry-pick the 7 new entries). The merge script covers +already-installed machines on `nosignal-update`. + +## Icon note +`kdenlive` uses `movie_edit` and `pi` uses `robot_2` — newer Material Symbols. +If the shipped icon font is older, fall back to `movie` and `smart_toy`. diff --git a/os updates/additions-extra/additions.json b/os updates/additions-extra/additions.json new file mode 100755 index 0000000..55b8749 --- /dev/null +++ b/os updates/additions-extra/additions.json @@ -0,0 +1,125 @@ +{ + "_comment": "NoSignal additions manifest — drives Settings → Additions. Each item: id (slug), name, desc (one line, shown until installed), icon (Material Symbol), check (sh, exit 0 = installed), install (sh, runs in a visible floating terminal — interactive sudo is fine). Sources policy: official pacman repos ([extra]/[omarchy]) or official upstream installers/git only — no AUR, no Flatpak.", + "items": [ + { + "id": "deckshift", + "name": "DeckShift Gaming Mode", + "desc": "Steam Big Picture gamescope session (Super+Shift+S)", + "icon": "sports_esports", + "check": "test -x /usr/local/bin/switch-to-gaming", + "install": "set -e; [ -d \"$HOME/deckshift/.git\" ] || git clone https://git.no-signal.uk/nosignal/deckshift.git \"$HOME/deckshift\"; cd \"$HOME/deckshift\"; chmod +x deckshift.sh; ./deckshift.sh; sh \"$HOME/.local/share/nosignal/deckshift-login/install-deckshift-login.sh\"" + }, + { + "id": "spotify", + "name": "Spotify", + "desc": "Music streaming client", + "icon": "music_note", + "check": "pacman -Q spotify", + "install": "sudo pacman -S --needed --noconfirm spotify" + }, + { + "id": "once", + "name": "Once", + "desc": "Basecamp self-hosted web app manager", + "icon": "deployed_code", + "check": "pacman -Q once-bin", + "install": "sudo pacman -S --needed --noconfirm once-bin" + }, + { + "id": "obsidian", + "name": "Obsidian", + "desc": "Knowledge base and Markdown notes", + "icon": "edit_note", + "check": "pacman -Q obsidian", + "install": "sudo pacman -S --needed --noconfirm obsidian" + }, + { + "id": "obs", + "name": "OBS Studio", + "desc": "Screen recording and streaming (+ hardware encoders)", + "icon": "videocam", + "check": "sh \"$HOME/.local/share/nosignal/additions-installer/obs-extras.sh\" check", + "install": "sh \"$HOME/.local/share/nosignal/additions-installer/obs-extras.sh\" install" + }, + { + "id": "claude-code", + "name": "Claude Code", + "desc": "Anthropic terminal coding agent", + "icon": "smart_toy", + "check": "command -v claude", + "install": "curl -fsSL https://claude.ai/install.sh | bash" + }, + { + "id": "codex", + "name": "Codex CLI", + "desc": "OpenAI terminal coding agent", + "icon": "code", + "check": "command -v codex", + "install": "curl -fsSL https://chatgpt.com/codex/install.sh | sh" + }, + { + "id": "opencode", + "name": "opencode", + "desc": "Open-source terminal coding agent", + "icon": "data_object", + "check": "command -v opencode", + "install": "curl -fsSL https://opencode.ai/install | bash" + }, + { + "id": "ollama", + "name": "Ollama", + "desc": "Run LLMs locally (GPU-accelerated)", + "icon": "neurology", + "check": "pacman -Q ollama", + "install": "gpu=$(lspci -nn 2>/dev/null | grep -iE 'VGA|3D|Display'); if echo \"$gpu\" | grep -qi '10de'; then pkg=ollama-cuda; elif echo \"$gpu\" | grep -qi '1002'; then pkg=ollama-rocm; else pkg=ollama; fi; echo \"Installing $pkg\"; sudo pacman -S --needed --noconfirm \"$pkg\"" + }, + { + "id": "lmstudio", + "name": "LM Studio", + "desc": "Desktop app to run LLMs locally (GPU-accelerated)", + "icon": "psychology", + "check": "pacman -Q lmstudio-bin || pacman -Q lmstudio", + "install": "sudo pacman -S --needed --noconfirm lmstudio-bin" + }, + { + "id": "dropbox", + "name": "Dropbox", + "desc": "Cloud file sync and sharing", + "icon": "cloud_sync", + "check": "pacman -Q dropbox", + "install": "sudo pacman -S --needed --noconfirm dropbox dropbox-cli nautilus-dropbox; systemctl --user enable --now dropbox 2>/dev/null || true; echo 'Dropbox installed — it will open a browser to link your account on first start.'" + }, + { + "id": "tailscale", + "name": "Tailscale", + "desc": "Mesh VPN to connect your devices", + "icon": "vpn_lock", + "check": "pacman -Q tailscale", + "install": "sudo pacman -S --needed --noconfirm tailscale; sudo systemctl enable --now tailscaled; echo 'Tailscale installed. Connect with: sudo tailscale up'" + }, + { + "id": "pinta", + "name": "Pinta", + "desc": "Simple image editor (Paint.NET-style)", + "icon": "brush", + "check": "pacman -Q pinta", + "install": "sudo pacman -S --needed --noconfirm pinta" + }, + { + "id": "kdenlive", + "name": "Kdenlive", + "desc": "Video editor (hardware-accelerated export)", + "icon": "movie_edit", + "check": "pacman -Q kdenlive", + "install": "sudo pacman -S --needed --noconfirm kdenlive" + }, + { + "id": "pi", + "name": "Pi Coding Agent", + "desc": "Interactive AI coding agent (multi-provider)", + "icon": "robot_2", + "check": "command -v pi", + "install": "curl -fsSL https://pi.dev/install.sh | sh" + } + ] +} diff --git a/os updates/additions-extra/merge-additions.sh b/os updates/additions-extra/merge-additions.sh new file mode 100755 index 0000000..1ef2e28 --- /dev/null +++ b/os updates/additions-extra/merge-additions.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# merge-additions.sh — idempotent. User-level (no root). +# +# Adds this component's additions.json items (by id) into the installed +# Settings -> Additions manifest, skipping ones already present. For the ISO the +# builder should instead ship this additions.json as the canonical manifest. +set -eu +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +LIVE="$HERE/../additions-installer/additions.json" +SRC="$HERE/additions.json" +[ -f "$LIVE" ] || { echo "live additions.json not found at $LIVE" >&2; exit 1; } +[ -f "$SRC" ] || { echo "component additions.json missing" >&2; exit 1; } +cp -a "$LIVE" "$LIVE.bak.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true +python3 - "$LIVE" "$SRC" <<'PY' +import json,sys +live_p,src_p=sys.argv[1:3] +live=json.load(open(live_p)); src=json.load(open(src_p)) +have={i["id"] for i in live["items"]} +added=[i for i in src["items"] if i["id"] not in have] +live["items"].extend(added) +json.dump(live,open(live_p,"w"),indent=2,ensure_ascii=False); open(live_p,"a").write("\n") +print("merged:", ", ".join(i["id"] for i in added) if added else "(none new)") +print("total items:", len(live["items"])) +PY +echo "additions-extra: ok" diff --git a/os updates/additions-installer/AdditionsPage.qml b/os updates/additions-installer/AdditionsPage.qml new file mode 100755 index 0000000..a952962 --- /dev/null +++ b/os updates/additions-installer/AdditionsPage.qml @@ -0,0 +1,120 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Io +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.nexus.common + +// NoSignal: the Additions settings page (replaces the upstream Plugins +// placeholder). Optional software installed on demand from official sources +// (pacman repos / upstream installers — no AUR, no Flatpak). Items come from +// the additions.json manifest via the status cache written by +// nosignal-additions; Install runs in a visible floating terminal so +// git/sudo/pacman output and prompts stay in front of the user. +PageBase { + id: root + + title: qsTr("Additions") + + property var status: ({}) + readonly property var items: status.items || [] + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // The Process objects live INSIDE the layout (its `data` accepts + // non-visual objects) — PageBase's default property is a single + // `Item`, so declaring them at page level kills the whole shell. + // Same pattern as UpdatesPage / the upstream AboutPage. + + // Read the cached status (instant). + Process { + id: readProc + + running: true + command: ["sh", "-c", "cat \"${XDG_STATE_HOME:-$HOME/.local/state}/nosignal/additions-status.json\" 2>/dev/null"] + stdout: StdioCollector { + onStreamFinished: { + try { + root.status = JSON.parse(text); + } catch (e) { + root.status = {}; + } + } + } + } + + // Refresh the cache on demand (re-runs every item's check). + Process { + id: checkProc + + command: ["sh", "-c", "\"$HOME/.local/bin/nosignal-additions\" status >/dev/null 2>&1"] + onExited: readProc.running = true + } + + // Run an installer in a visible floating terminal. + Process { + id: installProc + + property string addId: "" + + command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "\"$HOME/.local/bin/nosignal-additions\" install " + addId + "; printf '\\nPress Enter to close...'; read _"] + onExited: readProc.running = true + } + + SectionHeader { + text: qsTr("Optional software") + } + + Repeater { + model: root.items + + NavRow { + required property var modelData + required property int index + + first: index === 0 + last: index === root.items.length - 1 + icon: modelData.icon || "extension" + label: modelData.name + status: modelData.installed ? qsTr("Installed") : modelData.desc + onClicked: { + if (!modelData.installed && !installProc.running) { + installProc.addId = modelData.id; + installProc.running = true; + } + } + } + } + + InfoRow { + visible: root.items.length === 0 + first: true + last: true + label: qsTr("No additions manifest") + value: "—" + } + + SectionHeader { + text: qsTr("Actions") + } + + NavRow { + first: true + last: true + icon: "refresh" + label: qsTr("Re-check installed state") + status: installProc.running ? qsTr("Install running in terminal…") : (checkProc.running ? qsTr("Checking…") : qsTr("Refreshes the list above")) + onClicked: { + if (!checkProc.running) + checkProc.running = true; + } + } + } +} diff --git a/os updates/additions-installer/README.md b/os updates/additions-installer/README.md new file mode 100755 index 0000000..b1f9724 --- /dev/null +++ b/os updates/additions-installer/README.md @@ -0,0 +1,91 @@ +# additions-installer — Settings → Additions (optional software on demand) + +Turns the Settings app's placeholder **Plugins** section +into an **Additions** page: a curated list of optional software with +one-click installs. + +## Sources policy + +**No AUR. No Flatpak.** Official pacman repos or official upstream +installers/git only: + +| Item | Source | +|------|--------| +| DeckShift Gaming Mode | official git — `https://git.no-signal.uk/nosignal/deckshift.git` → `deckshift.sh` → **deckshift-login installer** (the one-shot-autologin fix, always run after deckshift.sh) | +| Spotify | `[omarchy]` pacman repo (proprietary — no upstream git exists) | +| Once (Basecamp) | `[omarchy]` pacman repo (`once-bin`) | +| Obsidian | Arch `[extra]` | +| OBS Studio | Arch `[extra]` (`obs-studio`) + **GPU-detected encoder runtimes** via `obs-extras.sh` (see below) | +| Claude Code | official installer — `curl -fsSL https://claude.ai/install.sh \| bash` | +| Codex CLI | official installer — `curl -fsSL https://chatgpt.com/codex/install.sh \| sh` | +| opencode | official installer — `curl -fsSL https://opencode.ai/install \| bash` | + +## OBS hardware encoders (obs-extras.sh) + +First-round testing found `obs-studio` alone ships only the x264 +software encoder path. The ISO lands on arbitrary GPUs (AMD / Intel / +NVIDIA, any mix), so the OBS entry delegates check AND install to +**`obs-extras.sh`**, which detects GPUs via `lspci` per machine: + +- **AMD** — VAAPI (H.264/HEVC/AV1) ships inside mesa: nothing to add. +- **Intel** — installs `intel-media-driver` + `vpl-gpu-rt` (QSV). +- **NVIDIA** — NVENC comes with the proprietary driver; deliberately + not auto-installed (driver branch is a system decision — see + gaming-enablement); prints a pointer if the driver is absent. + +Because the *check* also requires the detected runtimes, an OBS +installed without them shows as not-installed — clicking Install +completes the set idempotently. + +## How it works (same mechanism as the Updates page) + +- **`additions.json`** — the manifest, single source of truth. Per item: + `id`, `name`, `desc`, `icon` (Material Symbol), `check` (sh, exit 0 = + installed), `install` (sh). Edit this file to add/remove items — no + code changes needed. +- **`nosignal-additions`** (`~/.local/bin`) — `status` re-runs every check + and writes `~/.local/state/nosignal/additions-status.json`; + `install ` runs the item's installer then re-statuses;` list`. +- **`AdditionsPage.qml`** — reads the status cache; each item is a row + (Installed ✓ or description + click-to-install). Installs run in a + visible floating terminal (`kitty --class TUI.float`) so git/sudo/ + pacman prompts stay in front of the user. Modeled line-for-line on + UpdatesPage (Process objects inside the layout — see the comment). +- **`patch-additions-page.sh`** — root-run, idempotent: installs the + page QML, swaps the *remaining* System `PlaceholderComp` (the Plugins + stub) for the page, and relabels the menu entry + Plugins → Additions / "Install optional software". + **Ordering: requires the Updates-page patch first** — before it, + the first placeholder is the Updates stub (guarded: warns and skips). + Backups at `*.pre-nosignal-additions`; warns and degrades gracefully on + upstream drift. +- **pacman hook** (`nosignal-additions-page.hook`) re-applies after every + caelestia-shell upgrade. + +## Packaging + +- Ship the component in the layer tree + COPY-list; migration + `1781420400-additions-installer.sh` (sorts after the cliamp-music migration; add to the + baked `applied` list). +- The patch can be baked at image build (after the Updates-page patch). +- Do NOT pre-install any of the additions — the page is the opt-in. + +## Version pins / drift + +- Registry patch pinned against caelestia-shell **2.0.2** (same pin as + the Updates page; both patch scripts warn-and-degrade on drift). +- The three `curl | sh` installers and the DeckShift git URL are + upstream-controlled endpoints; if one moves, only `additions.json` + needs editing. + +## Test + +1. Settings → System shows **Additions** (not "Plugins"); page lists 8 + items with sensible installed/not-installed state. +2. Install something small (e.g. Obsidian) → floating terminal, sudo + prompt, pacman runs → row flips to "Installed" after the terminal + closes. +3. "Re-check installed state" refreshes after installing/removing + something from a terminal. +4. DeckShift entry: full chain (clone → deckshift.sh → deckshift-login + fix), then the Gaming Mode checklist items apply (Super+Shift+S etc.). diff --git a/os updates/additions-installer/additions.json b/os updates/additions-installer/additions.json new file mode 100755 index 0000000..ebce256 --- /dev/null +++ b/os updates/additions-installer/additions.json @@ -0,0 +1,125 @@ +{ + "_comment": "NoSignal additions manifest — drives Settings → Additions. Each item: id (slug), name, desc (one line, shown until installed), icon (Material Symbol), check (sh, exit 0 = installed), install (sh, runs in a visible floating terminal — interactive sudo is fine). Sources policy: official pacman repos ([extra]/[omarchy]) or official upstream installers/git only — no AUR, no Flatpak.", + "items": [ + { + "id": "deckshift", + "name": "DeckShift Gaming Mode", + "desc": "Steam Big Picture gamescope session (Super+Shift+S)", + "icon": "sports_esports", + "check": "test -x /usr/local/bin/switch-to-gaming", + "install": "set -e; [ -d \"$HOME/deckshift/.git\" ] || git clone https://git.no-signal.uk/nosignal/deckshift.git \"$HOME/deckshift\"; cd \"$HOME/deckshift\"; chmod +x deckshift.sh; ./deckshift.sh; sh \"$HOME/.local/share/nosignal/deckshift-login/install-deckshift-login.sh\"" + }, + { + "id": "spotify", + "name": "Spotify", + "desc": "Music streaming client", + "icon": "music_note", + "check": "pacman -Q spotify", + "install": "sudo pacman -S --needed --noconfirm spotify" + }, + { + "id": "once", + "name": "Once", + "desc": "Basecamp self-hosted web app manager (installs + starts Docker)", + "icon": "deployed_code", + "check": "pacman -Q once-bin", + "install": "sudo pacman -S --needed --noconfirm once-bin docker; sudo systemctl enable --now docker.service; sudo usermod -aG docker \"$USER\"; echo 'Once + Docker installed and Docker is running. Log out and back in (or run: newgrp docker) so docker works without sudo.'" + }, + { + "id": "obsidian", + "name": "Obsidian", + "desc": "Knowledge base and Markdown notes", + "icon": "edit_note", + "check": "pacman -Q obsidian", + "install": "sudo pacman -S --needed --noconfirm obsidian" + }, + { + "id": "obs", + "name": "OBS Studio", + "desc": "Screen recording and streaming (+ hardware encoders)", + "icon": "videocam", + "check": "sh \"$HOME/.local/share/nosignal/additions-installer/obs-extras.sh\" check", + "install": "sh \"$HOME/.local/share/nosignal/additions-installer/obs-extras.sh\" install" + }, + { + "id": "claude-code", + "name": "Claude Code", + "desc": "Anthropic terminal coding agent", + "icon": "smart_toy", + "check": "command -v claude", + "install": "curl -fsSL https://claude.ai/install.sh | bash" + }, + { + "id": "codex", + "name": "Codex CLI", + "desc": "OpenAI terminal coding agent", + "icon": "code", + "check": "command -v codex", + "install": "curl -fsSL https://chatgpt.com/codex/install.sh | sh" + }, + { + "id": "opencode", + "name": "opencode", + "desc": "Open-source terminal coding agent", + "icon": "data_object", + "check": "command -v opencode", + "install": "curl -fsSL https://opencode.ai/install | bash" + }, + { + "id": "ollama", + "name": "Ollama", + "desc": "Run LLMs locally (GPU-accelerated)", + "icon": "neurology", + "check": "pacman -Q ollama", + "install": "gpu=$(lspci -nn 2>/dev/null | grep -iE 'VGA|3D|Display'); if echo \"$gpu\" | grep -qi '10de'; then pkg=ollama-cuda; elif echo \"$gpu\" | grep -qi '1002'; then pkg=ollama-rocm; else pkg=ollama; fi; echo \"Installing $pkg\"; sudo pacman -S --needed --noconfirm \"$pkg\"" + }, + { + "id": "lmstudio", + "name": "LM Studio", + "desc": "Desktop app to run LLMs locally (GPU-accelerated)", + "icon": "psychology", + "check": "pacman -Q lmstudio-bin || pacman -Q lmstudio", + "install": "sudo pacman -S --needed --noconfirm lmstudio-bin" + }, + { + "id": "dropbox", + "name": "Dropbox", + "desc": "Cloud file sync and sharing", + "icon": "cloud_sync", + "check": "pacman -Q dropbox", + "install": "sudo pacman -S --needed --noconfirm dropbox dropbox-cli nautilus-dropbox; systemctl --user enable --now dropbox 2>/dev/null || true; echo 'Dropbox installed — it will open a browser to link your account on first start.'" + }, + { + "id": "tailscale", + "name": "Tailscale", + "desc": "Mesh VPN to connect your devices", + "icon": "vpn_lock", + "check": "pacman -Q tailscale", + "install": "sudo pacman -S --needed --noconfirm tailscale; sudo systemctl enable --now tailscaled; echo 'Tailscale installed. Connect with: sudo tailscale up'" + }, + { + "id": "pinta", + "name": "Pinta", + "desc": "Simple image editor (Paint.NET-style)", + "icon": "brush", + "check": "pacman -Q pinta", + "install": "sudo pacman -S --needed --noconfirm pinta" + }, + { + "id": "kdenlive", + "name": "Kdenlive", + "desc": "Video editor (hardware-accelerated export)", + "icon": "movie_edit", + "check": "pacman -Q kdenlive", + "install": "sudo pacman -S --needed --noconfirm kdenlive" + }, + { + "id": "pi", + "name": "Pi Coding Agent", + "desc": "Interactive AI coding agent (multi-provider)", + "icon": "robot_2", + "check": "command -v pi", + "install": "curl -fsSL https://pi.dev/install.sh | sh" + } + ] +} diff --git a/os updates/additions-installer/install-additions-installer.sh b/os updates/additions-installer/install-additions-installer.sh new file mode 100755 index 0000000..385f077 --- /dev/null +++ b/os updates/additions-installer/install-additions-installer.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# install-additions-installer.sh — Settings → Additions page: install +# optional software on demand from official sources (no AUR, no Flatpak). +# +# - nosignal-additions -> ~/.local/bin (status cache + installer runner) +# - additions.json -> stable on-system copy (the manifest) +# - AdditionsPage.qml -> patched into caelestia-shell (sudo) +# - pacman hook -> re-applies the patch after shell upgrades +# +# Safe to re-run (idempotent). Needs sudo for the QML patch + hook. +set -eu + +SRC=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +BIN="$HOME/.local/bin" +SHARE="$HOME/.local/share/nosignal/additions-installer" + +# 1. Backend + manifest. +mkdir -p "$BIN" "$SHARE" +install -m 0755 "$SRC/nosignal-additions" "$BIN/nosignal-additions" +install -m 0644 "$SRC/additions.json" "$SHARE/additions.json" +install -m 0755 "$SRC/obs-extras.sh" "$SHARE/obs-extras.sh" + +# 2. Stable on-system copies for the pacman hook to point at. +install -m 0644 "$SRC/AdditionsPage.qml" "$SHARE/AdditionsPage.qml" +install -m 0755 "$SRC/patch-additions-page.sh" "$SHARE/patch-additions-page.sh" + +# 3. Apply the QML patch now. +sudo sh "$SHARE/patch-additions-page.sh" + +# 4. Pacman hook: caelestia-shell upgrades revert both registries — re-patch. +HOOK=/etc/pacman.d/hooks/nosignal-additions-page.hook +sudo mkdir -p /etc/pacman.d/hooks +sudo tee "$HOOK" > /dev/null < $HOOK" + +# 5. Seed the status cache so the page has data on first open. +"$BIN/nosignal-additions" status >/dev/null 2>&1 || true + +echo "Done. Restart the shell (Ctrl+Super+Alt+R) to see Settings -> Additions." diff --git a/os updates/additions-installer/nosignal-additions b/os updates/additions-installer/nosignal-additions new file mode 100755 index 0000000..f9970d9 --- /dev/null +++ b/os updates/additions-installer/nosignal-additions @@ -0,0 +1,58 @@ +#!/bin/sh +# nosignal-additions — backend for Settings → Additions (optional software). +# The manifest (additions.json, next to this component's on-system copy) is +# the single source of truth: id/name/desc/icon per item, a `check` command +# (exit 0 = installed) and an `install` command (run in a visible terminal). +# +# nosignal-additions status rewrite the status cache (JSON) +# nosignal-additions list list manifest items (idname) +# nosignal-additions install run the item's installer, then re-status +# +# Sources policy: official pacman repos or official upstream installers/git +# only — no AUR, no Flatpak. +set -eu + +SHARE="$HOME/.local/share/nosignal/additions-installer" +MANIFEST="${NOSIGNAL_ADDITIONS_MANIFEST:-$SHARE/additions.json}" +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/nosignal" +STATE="$STATE_DIR/additions-status.json" + +[ -f "$MANIFEST" ] || { echo "nosignal-additions: manifest not found: $MANIFEST" >&2; exit 1; } + +status() { + mkdir -p "$STATE_DIR" + { + printf '{"checked":"%s","items":[' "$(date -Iseconds)" + sep="" + jq -c '.items[]' "$MANIFEST" | while IFS= read -r item; do + check=$(printf '%s' "$item" | jq -r '.check') + if sh -c "$check" >/dev/null 2>&1; then inst=true; else inst=false; fi + printf '%s' "$sep" + printf '%s' "$item" | jq -c --argjson inst "$inst" 'del(.check, .install) + {installed: $inst}' + sep="," + done + printf ']}\n' + } > "$STATE.tmp" + jq -e . "$STATE.tmp" >/dev/null # never replace the cache with broken JSON + mv "$STATE.tmp" "$STATE" +} + +install_one() { + id="$1" + item=$(jq -ce --arg id "$id" '.items[] | select(.id == $id)' "$MANIFEST") \ + || { echo "nosignal-additions: unknown addition: $id" >&2; exit 1; } + name=$(printf '%s' "$item" | jq -r '.name') + cmd=$(printf '%s' "$item" | jq -r '.install') + printf '\033[1;34m::\033[0m installing %s\n' "$name" + sh -ec "$cmd" + printf '\033[1;34m::\033[0m %s — installer finished\n' "$name" + status +} + +case "${1:-}" in + status) status ;; + list) jq -r '.items[] | "\(.id)\t\(.name)"' "$MANIFEST" ;; + install) [ -n "${2:-}" ] || { echo "usage: nosignal-additions install " >&2; exit 2; } + install_one "$2" ;; + *) echo "usage: nosignal-additions status | list | install " >&2; exit 2 ;; +esac diff --git a/os updates/additions-installer/obs-extras.sh b/os updates/additions-installer/obs-extras.sh new file mode 100755 index 0000000..8e7ac4b --- /dev/null +++ b/os updates/additions-installer/obs-extras.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# obs-extras.sh — OBS Studio with the right hardware encoders for THIS box. +# The Additions page ships on every install and GPUs differ (AMD / Intel / +# NVIDIA, any mix — e.g. AMD discrete + Intel iGPU), so the +# encoder runtime packages are DETECTED per machine, not hardcoded. +# +# obs-extras.sh check exit 0 only if OBS + everything this box's GPUs +# need is installed (drives the Additions row state) +# obs-extras.sh install install OBS + the detected encoder runtimes +# +# Per-vendor encoder support on Arch: +# AMD — VAAPI (H.264/HEVC/AV1) ships inside mesa (base system): no extra +# package since the libva-mesa-driver merge. +# Intel — intel-media-driver (VAAPI) + vpl-gpu-rt (QuickSync/QSV runtime). +# NVIDIA — NVENC comes with the proprietary driver (nvidia-utils or a +# legacy branch). Deliberately NOT auto-installed here: the driver +# branch is a system-level decision (see gaming-enablement's +# NVIDIA notes) — we print a pointer instead. +set -eu + +gpus=$(lspci -nn 2>/dev/null | grep -Ei 'vga|3d|display' || true) + +needed="" +case "$gpus" in *[Ii]ntel*) needed="intel-media-driver vpl-gpu-rt" ;; esac + +check() { + pacman -Q obs-studio >/dev/null 2>&1 || return 1 + for p in $needed; do + pacman -Q "$p" >/dev/null 2>&1 || return 1 + done + return 0 +} + +do_install() { + # shellcheck disable=SC2086 — $needed is a deliberate word-split list + sudo pacman -S --needed --noconfirm obs-studio $needed + case "$gpus" in + *[Aa][Mm][Dd]*|*ATI*) + echo ":: AMD GPU: VAAPI encoders ship in mesa — already present." ;; + esac + case "$gpus" in + *[Nn][Vv][Ii][Dd][Ii][Aa]*) + if pacman -Q nvidia-utils >/dev/null 2>&1; then + echo ":: NVIDIA GPU: NVENC available via the installed driver." + else + echo "NOTE: NVIDIA GPU detected but no proprietary driver — NVENC needs" + echo " it (nvidia-utils, or a legacy branch for pre-Turing cards)." + fi ;; + esac + [ -n "$needed" ] && echo ":: Intel GPU: VAAPI + QuickSync runtimes installed." + echo ":: restart OBS to pick up new encoders." +} + +case "${1:-}" in + check) check ;; + install) do_install ;; + *) echo "usage: obs-extras.sh check|install" >&2; exit 2 ;; +esac diff --git a/os updates/additions-installer/patch-additions-page.sh b/os updates/additions-installer/patch-additions-page.sh new file mode 100755 index 0000000..6203f5f --- /dev/null +++ b/os updates/additions-installer/patch-additions-page.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# patch-additions-page.sh — register the NoSignal Additions page in +# caelestia-shell. Runs as root (via sudo from the installer, or via the +# pacman hook after every caelestia-shell upgrade, which reverts both +# registry files). Idempotent. +# +# What it does: +# 1. copies AdditionsPage.qml (next to this script) into the shell's +# pages/ dir (untracked file — pacman upgrades leave it alone) +# 2. swaps the REMAINING "System" PlaceholderComp in PageCompRegistry.qml +# (the Plugins stub) for a StackPage hosting AdditionsPage. +# REQUIRES the Updates-page patch to be applied first — before it, +# the first placeholder is the Updates stub, and this patch would grab +# the wrong slot (guarded below). +# 3. relabels the menu entry in PageRegistry.qml: "Plugins" → "Additions". +# +# NEXUS is overridable for testing the regexes against copies. +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +NEXUS=${NEXUS:-/etc/xdg/quickshell/caelestia/modules/nexus} +COMPREG="$NEXUS/PageCompRegistry.qml" +PAGEREG="$NEXUS/PageRegistry.qml" + +[ -f "$COMPREG" ] || { echo "caelestia-shell not found at $NEXUS — nothing to patch"; exit 0; } + +install -m 0644 "$SELF_DIR/AdditionsPage.qml" "$NEXUS/pages/AdditionsPage.qml" + +# --- component registry: Plugins placeholder -> AdditionsPage --------------- +if grep -q 'AdditionsPage' "$COMPREG"; then + echo ":: component registry already patched" +elif ! grep -q 'UpdatesPage' "$COMPREG"; then + echo "WARNING: Updates patch not applied — the first placeholder" >&2 + echo " is the Updates stub, not Plugins. Run the updates-panel patch" >&2 + echo " first, then re-run $(basename "$0")." >&2 +else + cp -n "$COMPREG" "$COMPREG.pre-nosignal-additions" + perl -0pi -e 's/Component \{\s*\n\s*PlaceholderComp \{\}\s*\n\s*\},/Component {\n \/\/ Additions (NoSignal)\n StackPage {\n Component {\n AdditionsPage {}\n }\n }\n },/' "$COMPREG" + if grep -q 'AdditionsPage' "$COMPREG"; then + echo ":: patched $COMPREG (Additions page registered)" + else + echo "WARNING: patch did not apply — upstream PageCompRegistry.qml changed" >&2 + echo " shape. The Additions page is installed but not registered;" >&2 + echo " update the regex in $(basename "$0")." >&2 + fi +fi + +# --- menu registry: relabel Plugins -> Additions ----------------------------- +if [ -f "$PAGEREG" ]; then + if grep -q '"Additions"' "$PAGEREG"; then + echo ":: menu entry already relabeled" + else + cp -n "$PAGEREG" "$PAGEREG.pre-nosignal-additions" + perl -pi -e 's/label: qsTr\("Plugins"\),/label: qsTr("Additions"),/; s/description: qsTr\("Manage plugins"\),/description: qsTr("Install optional software"),/' "$PAGEREG" + if grep -q '"Additions"' "$PAGEREG"; then + echo ":: relabeled menu entry Plugins -> Additions" + else + echo "WARNING: relabel did not apply (upstream PageRegistry.qml changed)." >&2 + echo " Cosmetic only — the page still works under the old label." >&2 + fi + fi +fi diff --git a/os updates/app-theme-awareness/README.md b/os updates/app-theme-awareness/README.md new file mode 100755 index 0000000..bf5c081 --- /dev/null +++ b/os updates/app-theme-awareness/README.md @@ -0,0 +1,67 @@ +# app-theme-awareness + +User-level, no root. + +## Goal +Make external apps (Chrome/Chromium, Electron, Firefox, GTK, Qt) **follow +Caelestia's light/dark mode** automatically — dark when Caelestia is dark. + +## Why they don't today +Modern apps read one signal: `org.freedesktop.appearance` → `color-scheme`, +published by **xdg-desktop-portal**. By default that value is **0 +("no preference")** because nothing sets it — `gsettings +org.gnome.desktop.interface color-scheme` is `'default'`. So every app defaults +to light. All portal pieces are already installed (`xdg-desktop-portal`, +`-gtk`, `-hyprland`); `libadwaita` is present. + +## How this works +- **`nosignal-app-theme-sync`**: reads `caelestia scheme get` ("Mode: dark| + light") and sets: + - `gsettings org.gnome.desktop.interface color-scheme` → `prefer-dark` / + `prefer-light`. xdg-desktop-portal-gtk republishes this as the freedesktop + appearance `color-scheme`, which **Chrome, Electron, Firefox, GTK4/libadwaita + and Qt6 honor** (UI + web `prefer-color-scheme`). + - `gtk-application-prefer-dark-theme` in `gtk-3.0`/`gtk-4.0` `settings.ini` + for legacy GTK3 apps that ignore the portal. +- **`nosignal-app-theme.path` + `.service`** (user units, mirroring NoSignal's + `hyprmoncfgd-rescan.path`): watch `~/.local/state/caelestia/scheme.json` and + re-run the sync on every scheme change, and run it once at login. So flipping + Caelestia light↔dark flips the apps live. +- **`portals.conf`**: pins the Settings portal backend to `gtk` (keeping + Hyprland's portal default for screencast), so the appearance value is served + reliably. Installed only if the user has no `portals.conf`. + +`install-app-theme-awareness.sh` does all of it idempotently — entirely +user-level (no root, no pacman hook). + +## Per-app notes +- **Chrome/Chromium (Wayland):** follows the portal automatically. If the UI + stays light, set `chrome://settings` → Appearance → **GTK / "Use system"**. +- **Qt:** `QT_QPA_PLATFORMTHEME=qtengine` is already set; Qt6 honors the portal. + Old Qt5 apps may need a dark qt5ct/Kvantum style (out of scope). +- **GTK3 dark fidelity:** prefer-dark uses Adwaita's built-in dark rendering. For + a fuller dark GTK3 theme the builder can add `gnome-themes-extra` (ships + `Adwaita-dark`) — optional. + +## Builder integration +Ship the sync script in the layer bin, the two user units (enabled by default, +like the other NoSignal user units), and seed `portals.conf`. Ideally Caelestia +itself could set `color-scheme` when its scheme flips; until then these units +bridge it. Mode is read from `caelestia scheme get`, so it tracks the dynamic +Material scheme. + +## Verify (after install) +``` +caelestia scheme set -m light # or via the Caelestia UI +gsettings get org.gnome.desktop.interface color-scheme # -> 'prefer-light' +caelestia scheme set -m dark +gsettings get org.gnome.desktop.interface color-scheme # -> 'prefer-dark' +# Chrome/GTK apps follow within a moment. +``` + +## Files +- `nosignal-app-theme-sync` — mode → portal/GTK sync. +- `nosignal-app-theme.service` / `.path` — login run + watch-on-change. +- `portals.conf` — Settings backend = gtk. +- `install-app-theme-awareness.sh` — idempotent user-level installer. +- `migrations/1781442000-app-theme-awareness.sh` — delegates to it. diff --git a/os updates/app-theme-awareness/install-app-theme-awareness.sh b/os updates/app-theme-awareness/install-app-theme-awareness.sh new file mode 100755 index 0000000..b799c3c --- /dev/null +++ b/os updates/app-theme-awareness/install-app-theme-awareness.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# install-app-theme-awareness.sh — idempotent. User-level (no root). +# +# External apps (Chrome, Electron, Firefox, GTK, Qt) don't follow +# the desktop theme because nothing publishes a color-scheme preference — the +# portal reports "no preference" (0). This installs a sync that mirrors +# Caelestia's light/dark mode to the freedesktop appearance portal + GTK, and +# units that re-run it on every scheme change. +set -eu + +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +BIN="$HOME/.local/bin" +UNITS="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" +PORTAL="${XDG_CONFIG_HOME:-$HOME/.config}/xdg-desktop-portal" + +install -Dm0755 "$HERE/nosignal-app-theme-sync" "$BIN/nosignal-app-theme-sync" +install -Dm0644 "$HERE/nosignal-app-theme.service" "$UNITS/nosignal-app-theme.service" +install -Dm0644 "$HERE/nosignal-app-theme.path" "$UNITS/nosignal-app-theme.path" +echo "installed sync script + user units" + +# portals.conf: only create if the user doesn't already have one (don't clobber) +if [ ! -e "$PORTAL/portals.conf" ]; then + install -Dm0644 "$HERE/portals.conf" "$PORTAL/portals.conf" + echo "installed $PORTAL/portals.conf (Settings=gtk)" +else + echo "note: $PORTAL/portals.conf exists — leaving it; ensure it has org.freedesktop.impl.portal.Settings=gtk" +fi + +# enable the watcher + run an initial sync (needs a running user systemd bus) +if systemctl --user show-environment >/dev/null 2>&1; then + systemctl --user daemon-reload + systemctl --user enable --now nosignal-app-theme.path nosignal-app-theme.service || true + echo "enabled nosignal-app-theme.{path,service}" +else + echo "note: no user systemd bus here — units installed; they start at next login" + "$BIN/nosignal-app-theme-sync" || true +fi + +echo "app-theme-awareness: ok" diff --git a/os updates/app-theme-awareness/nosignal-app-theme-sync b/os updates/app-theme-awareness/nosignal-app-theme-sync new file mode 100755 index 0000000..6138795 --- /dev/null +++ b/os updates/app-theme-awareness/nosignal-app-theme-sync @@ -0,0 +1,39 @@ +#!/bin/sh +# nosignal-app-theme-sync — make external apps follow Caelestia's light/dark mode. +# +# Reads Caelestia's current mode (`caelestia scheme get` -> "Mode: dark|light") +# and publishes it so toolkit apps follow: +# * gsettings color-scheme -> xdg-desktop-portal(-gtk) org.freedesktop.appearance +# => Chrome/Chromium, Electron, Firefox, GTK4/libadwaita, Qt6 all follow. +# * GTK3 prefer-dark flag in gtk-3.0/gtk-4.0 settings.ini for legacy GTK apps +# that don't read the portal. +# Idempotent; safe to run repeatedly. Invoked at login and on scheme change by +# the nosignal-app-theme .service/.path units. +set -u + +mode=$(caelestia scheme get 2>/dev/null | awk -F': *' '/Mode:/{print tolower($2); exit}') +case "$mode" in + dark) cs=prefer-dark; pd=true ;; + light) cs=prefer-light; pd=false ;; + *) cs=default; pd=false ;; # unknown -> no preference +esac + +# 1. master switch (portal): Chrome / Electron / Firefox / GTK4 / Qt6 +if command -v gsettings >/dev/null 2>&1; then + gsettings set org.gnome.desktop.interface color-scheme "$cs" 2>/dev/null || true +fi + +# 2. legacy GTK3 (+GTK4) prefer-dark flag for apps that ignore the portal +for v in 3.0 4.0; do + d="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-$v"; f="$d/settings.ini" + mkdir -p "$d" + [ -f "$f" ] || printf '[Settings]\n' > "$f" + grep -q '^\[Settings\]' "$f" || printf '[Settings]\n' >> "$f" + if grep -q '^gtk-application-prefer-dark-theme' "$f"; then + sed -i "s/^gtk-application-prefer-dark-theme.*/gtk-application-prefer-dark-theme=$pd/" "$f" + else + printf 'gtk-application-prefer-dark-theme=%s\n' "$pd" >> "$f" + fi +done + +echo "app-theme: caelestia mode=${mode:-unknown} -> color-scheme=$cs, gtk prefer-dark=$pd" diff --git a/os updates/app-theme-awareness/nosignal-app-theme.path b/os updates/app-theme-awareness/nosignal-app-theme.path new file mode 100755 index 0000000..3f9a3c9 --- /dev/null +++ b/os updates/app-theme-awareness/nosignal-app-theme.path @@ -0,0 +1,9 @@ +[Unit] +Description=Watch Caelestia scheme and re-sync external app theme on change + +[Path] +PathChanged=%h/.local/state/caelestia/scheme.json +Unit=nosignal-app-theme.service + +[Install] +WantedBy=default.target diff --git a/os updates/app-theme-awareness/nosignal-app-theme.service b/os updates/app-theme-awareness/nosignal-app-theme.service new file mode 100755 index 0000000..ac6dc12 --- /dev/null +++ b/os updates/app-theme-awareness/nosignal-app-theme.service @@ -0,0 +1,9 @@ +[Unit] +Description=Sync external app theme (portal/GTK) to Caelestia's light/dark mode + +[Service] +Type=oneshot +ExecStart=%h/.local/bin/nosignal-app-theme-sync + +[Install] +WantedBy=default.target diff --git a/os updates/app-theme-awareness/portals.conf b/os updates/app-theme-awareness/portals.conf new file mode 100755 index 0000000..fca9a28 --- /dev/null +++ b/os updates/app-theme-awareness/portals.conf @@ -0,0 +1,8 @@ +# NoSignal: keep Hyprland's portal as default (screencast/screenshot) but let +# xdg-desktop-portal-gtk serve the Settings interface, so the +# org.freedesktop.appearance color-scheme (driven by gsettings) is published +# reliably to apps. Installed to ~/.config/xdg-desktop-portal/portals.conf +# only if no user portals.conf already exists. +[preferred] +default=hyprland;gtk +org.freedesktop.impl.portal.Settings=gtk diff --git a/os updates/base-default-packages/README.md b/os updates/base-default-packages/README.md new file mode 100755 index 0000000..d011918 --- /dev/null +++ b/os updates/base-default-packages/README.md @@ -0,0 +1,24 @@ +# base-default-packages (installed by default, not opt-in) + +The install command needs root — run it once. + +Packages that should ship **installed by default** in the base build (distinct +from the opt-in Settings → Additions). Official repos only; GPU-agnostic. + +| Package | Provides | Repo | Why | +|---------|----------|------|-----| +| `github-cli` | `gh` | extra | GitHub CLI — default dev tooling (PRs, issues, repo ops, auth) | + +## Builder integration +Add the package(s) above to the ISO base package set. `install-base-default- +packages.sh` covers already-installed machines (idempotent `pacman -S --needed`) +and is wired via the migration below. + +## Apply on an existing box +``` +sudo pacman -S --needed github-cli +``` + +## Files +- `install-base-default-packages.sh` — idempotent installer (root). +- `migrations/1781452800-base-default-packages.sh` — delegates to it. diff --git a/os updates/base-default-packages/install-base-default-packages.sh b/os updates/base-default-packages/install-base-default-packages.sh new file mode 100755 index 0000000..22599ea --- /dev/null +++ b/os updates/base-default-packages/install-base-default-packages.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# install-base-default-packages.sh — idempotent. Needs root (pacman). +# +# Ensures packages that should be installed BY DEFAULT in the base build (not +# opt-in Additions). For the ISO, the builder should add these to the base +# package set; this covers already-installed systems via nosignal-update. +set -eu + +# Base default packages (official repos; GPU-agnostic): +PKGS="github-cli" + +if [ "$(id -u)" -eq 0 ]; then + pacman -S --needed --noconfirm $PKGS +elif command -v sudo >/dev/null 2>&1; then + sudo pacman -S --needed --noconfirm $PKGS +else + echo "need root to install: $PKGS" >&2 + exit 1 +fi +echo "base default packages ensured: $PKGS" diff --git a/os updates/cachyos-repo-switch/02-nosignal-cachy b/os updates/cachyos-repo-switch/02-nosignal-cachy new file mode 100644 index 0000000..28ffdd7 --- /dev/null +++ b/os updates/cachyos-repo-switch/02-nosignal-cachy @@ -0,0 +1,3 @@ +# NoSignal: flip the CachyOS repos from the settings toggle without a password. +# Pinned to the fixed, root-owned helper, which itself only accepts detect/status/enable/disable. +%wheel ALL=(ALL) NOPASSWD: /usr/local/bin/nosignal-cachy-repo detect, /usr/local/bin/nosignal-cachy-repo status, /usr/local/bin/nosignal-cachy-repo enable, /usr/local/bin/nosignal-cachy-repo disable diff --git a/os updates/cachyos-repo-switch/CachyRepoToggleRow.qml b/os updates/cachyos-repo-switch/CachyRepoToggleRow.qml new file mode 100644 index 0000000..51375de --- /dev/null +++ b/os updates/cachyos-repo-switch/CachyRepoToggleRow.qml @@ -0,0 +1,73 @@ +// CachyRepoToggleRow.qml (NoSignal) — Settings -> Services toggle to enable / +// disable the CachyOS pacman repositories. Auto-detects the best x86-64 microarch +// tier the CPU supports (v4 > v3). Live state polls `nosignal-cachy-repo status` +// (no root). enable/disable run in a floating terminal so the pacman/download +// output stays visible; the grant is passwordless via sudo -n +// (/etc/sudoers.d/02-nosignal-cachy). enable adds repos, converts userspace to +// the optimized builds (pacman -Suu, pacman pinned stock), and installs the +// linux-cachyos kernel; disable reverts to stock and removes the cachy kernel +// (keeping stock `linux` bootable). A reboot switches the running kernel. +// +// Untracked file under modules/nexus/common — auto-discovered as the type +// `CachyRepoToggleRow` via `import qs.modules.nexus.common`. Survives caelestia +// upgrades; pairs with the one-line insert in ServicesPage.qml. +import QtQuick +import Quickshell.Io +import qs.modules.nexus.common + +ToggleRow { + id: root + + property bool repoOn: false // NB: not `enabled` (reserved Item property) + + text: qsTr("CachyOS repositories") + subtext: repoOn + ? qsTr("On — CachyOS optimized builds + linux-cachyos kernel (reboot to run it)") + : qsTr("Switch to CachyOS optimized builds + linux-cachyos kernel (reboot after)") + + onToggled: { + if (checked) + enableProc.running = true; + else + disableProc.running = true; + reconcile.restart(); + } + + // --- live state ----------------------------------------------------------- + Process { + id: statusProc + command: ["/usr/local/bin/nosignal-cachy-repo", "status"] + stdout: StdioCollector { + onStreamFinished: { + root.repoOn = text.trim() === "enabled"; + root.checked = root.repoOn; // drive switch from real state + } + } + } + + // --- actions (passwordless; shown in a terminal for pacman output) --------- + Process { + id: enableProc + command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "sudo -n /usr/local/bin/nosignal-cachy-repo enable; printf '\\nPress Enter to close...'; read _"] + onExited: reconcile.restart() + } + Process { + id: disableProc + command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "sudo -n /usr/local/bin/nosignal-cachy-repo disable; printf '\\nPress Enter to close...'; read _"] + onExited: reconcile.restart() + } + + // --- polling -------------------------------------------------------------- + Timer { + interval: 5000 + repeat: true + running: true + triggeredOnStart: true + onTriggered: statusProc.running = true + } + Timer { + id: reconcile + interval: 2000 + onTriggered: statusProc.running = true + } +} diff --git a/os updates/cachyos-repo-switch/README.md b/os updates/cachyos-repo-switch/README.md new file mode 100644 index 0000000..c25aa62 --- /dev/null +++ b/os updates/cachyos-repo-switch/README.md @@ -0,0 +1,79 @@ +# cachyos-repo-switch — CachyOS optimized repos + kernel switch (sudoless, auto-tier) + +Works **both directions** — ON → reboot into `linux-cachyos`; OFF → reboot back to stock. + +## What it is +One toggle in **Settings → Services** (under "Passwordless sudo") that switches the +system **to/from** CachyOS-optimized builds **and** the `linux-cachyos` kernel — +passwordless, auto-detecting the best x86-64 tier (v4 > v3), fully unattended +(`--noconfirm`). Refuses below x86-64-v3. + +## Pieces +- **`nosignal-cachy-repo`** → `/usr/local/bin` — `detect` / `status` / `enable` / + `disable` (+ `--dry-run`). +- **`02-nosignal-cachy`** → `/etc/sudoers.d` (0440, `visudo -c`-validated at install) + — `%wheel` NOPASSWD, pinned to the four exact subcommands of the fixed helper. +- **`CachyRepoToggleRow.qml`** → `nexus/common/` — baked into the pinned + nosignal-shell fork; this copy + `patch-servicespage.sh` are the migration + path for old stock-caelestia installs. +- **`patch-servicespage.sh`** — inserts the toggle into `ServicesPage.qml` (only used + by the nosignal-update migration; the builder skips it via NOSIGNAL_SKIP_SHELL_PATCH). + +## Two design fixes baked in — DO NOT reintroduce the bugs +1. **Architecture (critical).** CachyOS v3/v4 packages have arch `x86_64_v3`/`x86_64_v4`. + Upstream relies on its OWN pacman (reads `Architecture = auto` as "include v3/v4"). + We keep **stock pacman** (reads `auto` as just `x86_64`) → it would arch-reject every + v3/v4 package and the conversion would silently do nothing. So `enable` sets + `Architecture` EXPLICITLY to the tier (`x86_64 x86_64_v3 x86_64_v4` for v4, + `x86_64 x86_64_v3` for v3); `disable` restores `Architecture = auto`. +2. **Targeted revert (no whole-base pull).** `disable` reinstalls ONLY packages whose + local-DB `Packager` is `CachyOS …` (excluding the cachy keyring/mirrorlist pkgs, + removed separately) — never the full base. Nothing converted ⇒ nothing to do. + +## Why stock pacman (and `--ignore pacman`) +Upstream `cachyos-repo.sh` is interactive, ends with `pacman -Syu`, and swaps in a +CachyOS `pacman`. We reuse its signed keyring/mirrorlists + `.awk` stanza files but +drive pacman ourselves (stock, unattended). The CachyOS `pacman` build stamps an +`%INSTALLED_DB%` key into the local DB that stock pacman then warns about on every op, +so `enable` runs `pacman -Suu --ignore pacman` to pin pacman stock. **Keep that flag.** + +## Kernel guard +`enable` installs `linux-cachyos` + `-headers` (the limine-mkinitcpio-hook generates +the boot entry; stock `linux` stays as a fallback). `disable` removes the cachy kernel +ONLY after confirming stock `linux` is installed — never strips the last bootable +kernel. A **reboot** is needed after ON (to run the new kernel) and after OFF (to leave +it); pick the entry at the Limine menu. + +## Live-fetch vs vendored (build decision: LIVE-FETCH) +`enable`/`disable` fetch upstream `cachyos-repo.tar.xz` (keyring + mirrorlists + `.awk`). +Enabling CachyOS is inherently online (it pulls optimized builds + the kernel from +CachyOS mirrors), so there is no offline scenario to vendor for — live-fetch needs no +bundled artifacts and never goes stale. If a future air-gapped image needs it, vendor +the tarball and drop the fetch in `fetch_upstream()`. + +## CachyOS-exclusive packages don't revert (by design) +Any package with no stock equivalent stays a CachyOS build on OFF and is reported, not +removed (e.g. `quickshell-git`, `gamescope-session-git` — +harmless; self-clear on the next `yay -Sua`). The `linux-cachyos` kernel is the one +exception (removed explicitly, guarded as above). Snapshot entries keep historical +cachy UKIs under the Snapshots submenu (cosmetic; age out as snapshots rotate). + +## Closure deps +`curl`, `tar`/`xz`, `gawk` (all via Arch `base`), `kitty`, plus `limine` + +`limine-mkinitcpio-hook` for the kernel boot entry — all already on the NoSignal base. + +## Install (root) +``` +sudo sh ~/.local/share/nosignal/cachyos-repo-switch/install-cachyos-repo-switch.sh +``` +Then reload caelestia (or log out/in) so the toggle loads. + +## Use +- Settings → Services → **CachyOS repositories**: on opens a floating terminal showing + the full pacman transaction (passwordless); off reverts to stock. +- Or terminal: `sudo nosignal-cachy-repo enable` / `disable`, + `nosignal-cachy-repo status` / `detect` (add `--dry-run` to preview). + +## Files +- `nosignal-cachy-repo`, `02-nosignal-cachy`, `install-cachyos-repo-switch.sh`, + `CachyRepoToggleRow.qml`, `patch-servicespage.sh`. diff --git a/os updates/cachyos-repo-switch/install-cachyos-repo-switch.sh b/os updates/cachyos-repo-switch/install-cachyos-repo-switch.sh new file mode 100755 index 0000000..ff7ac83 --- /dev/null +++ b/os updates/cachyos-repo-switch/install-cachyos-repo-switch.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# install-cachyos-repo-switch.sh — idempotent. REQUIRES ROOT. +# +# Installs the CachyOS repo + kernel switch feature: +# 1. /usr/local/bin/nosignal-cachy-repo (detect/status/enable/disable CLI) +# 2. /etc/sudoers.d/02-nosignal-cachy (sudoless grant, validated first) +# 3. patches caelestia ServicesPage to show the toggle (via patch-servicespage.sh) +# +# Does NOT enable the CachyOS repos — that's a deliberate user action via the +# toggle (it is heavy, networked and reboots the kernel). +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +[ "$(id -u)" -eq 0 ] || { echo "must run as root (writes /usr/local/bin + sudoers.d)" >&2; exit 1; } + +# 1. CLI helper +install -m 0755 "$SELF_DIR/nosignal-cachy-repo" /usr/local/bin/nosignal-cachy-repo +echo ":: installed /usr/local/bin/nosignal-cachy-repo" + +# 1b. standalone DB repair tool. A normal -Syu while +# CachyOS is enabled pulls the CachyOS pacman, which stamps `%INSTALLED_DB%` +# into the local DB; stock pacman then warns about it on every op. The +# corrected helper above pins pacman durably (IgnorePkg) while enabled and +# strips the field on disable, but ship the standalone repair tool too — and +# one-shot-clean any contamination already present (no-op on a fresh box). +install -m 0755 "$SELF_DIR/nosignal-cachy-db-clean" /usr/local/bin/nosignal-cachy-db-clean +echo ":: installed /usr/local/bin/nosignal-cachy-db-clean" +if grep -lrx '%INSTALLED_DB%' /var/lib/pacman/local/*/desc >/dev/null 2>&1; then + echo ":: local DB has %INSTALLED_DB% contamination — cleaning now…" + /usr/local/bin/nosignal-cachy-db-clean || true +fi + +# 2. sudoers drop-in — validate with `visudo -c` BEFORE it reaches /etc/sudoers.d +# (a malformed sudoers can lock out sudo entirely). install to a temp path, +# check, then move into place at mode 0440. +TMP_SUDO=$(mktemp /tmp/02-nosignal-cachy.XXXXXX) +install -m 0440 "$SELF_DIR/02-nosignal-cachy" "$TMP_SUDO" +if visudo -cf "$TMP_SUDO" >/dev/null 2>&1; then + install -m 0440 -o root -g root "$TMP_SUDO" /etc/sudoers.d/02-nosignal-cachy + echo ":: installed /etc/sudoers.d/02-nosignal-cachy (validated)" +else + echo "ERROR: 02-nosignal-cachy failed visudo -c — NOT installing the sudoers drop-in." >&2 + echo " (the toggle will prompt for a password instead of running passwordless)" >&2 +fi +rm -f "$TMP_SUDO" + +# 3. panel toggle (caelestia ServicesPage). Non-fatal if the shell isn't present. +# Skipped when NOSIGNAL_SKIP_SHELL_PATCH is set — the NoSignal builder bakes +# CachyRepoToggleRow + the page insert into the pinned nosignal-shell fork, so +# the patch is redundant there. Migrations of old stock-caelestia boxes +# leave it unset and patch. +if [ -n "${NOSIGNAL_SKIP_SHELL_PATCH:-}" ]; then + echo ":: skipping ServicesPage patch (NOSIGNAL_SKIP_SHELL_PATCH — fork bakes the toggle)" +elif [ -x "$SELF_DIR/patch-servicespage.sh" ]; then + sh "$SELF_DIR/patch-servicespage.sh" || echo "WARNING: ServicesPage patch failed (CLI still usable from a terminal)" >&2 +fi + +echo ":: done. Toggle it in Settings -> Services, or: sudo nosignal-cachy-repo enable" diff --git a/os updates/cachyos-repo-switch/nosignal-cachy-db-clean b/os updates/cachyos-repo-switch/nosignal-cachy-db-clean new file mode 100755 index 0000000..bc71b00 --- /dev/null +++ b/os updates/cachyos-repo-switch/nosignal-cachy-db-clean @@ -0,0 +1,54 @@ +#!/bin/bash +# nosignal-cachy-db-clean — repair tool for boxes already contaminated by a +# CachyOS pacman run. +# +# Symptom this fixes: every pacman/yay op prints a flood of +# warning: : unknown key '%INSTALLED_DB%' in local database +# `%INSTALLED_DB%` is a field the CachyOS `pacman` build writes into the local DB; +# stock Arch pacman doesn't understand it and warns about it on EVERY operation. +# It is harmless (the DB is structurally valid — `pacman -Dk` passes — and +# transactions resolve fine), but it looks exactly like database corruption. +# +# This strips the `%INSTALLED_DB%` field from every /var/lib/pacman/local/*/desc. +# Each block is exactly: a `%INSTALLED_DB%` line + one value line + a blank +# separator. Idempotent — only rewrites files that contain the field; safe to +# re-run. Use --dry-run to preview. REQUIRES ROOT (writes the local DB) unless +# --dry-run. +set -u + +DB="${PACMAN_LOCAL_DB:-/var/lib/pacman/local}" +DRY=0 +[[ "${1:-}" == "--dry-run" ]] && DRY=1 + +if [[ $DRY -eq 0 && $EUID -ne 0 ]]; then + echo "Run as root (writes $DB), e.g.: sudo $0 — or preview with: $0 --dry-run" >&2 + exit 1 +fi + +mapfile -t hits < <(grep -lrx '%INSTALLED_DB%' "$DB"/*/desc 2>/dev/null) +if [[ ${#hits[@]} -eq 0 ]]; then + echo "Local pacman DB already clean — no %INSTALLED_DB% fields found." + exit 0 +fi + +echo "Found %INSTALLED_DB% in ${#hits[@]} package entr(y/ies)." +n=0 +for desc in "${hits[@]}"; do + if [[ $DRY -eq 1 ]]; then + echo "+ would strip: $desc" + n=$((n+1)); continue + fi + tmp="$desc.nsclean.$$" + if awk '/^%INSTALLED_DB%$/{s=1;next} s&&/^$/{s=0;next} s{next} {print}' "$desc" > "$tmp" \ + && chmod --reference="$desc" "$tmp" 2>/dev/null; then + mv -f "$tmp" "$desc"; n=$((n+1)) + else + rm -f "$tmp"; echo " ! failed to rewrite: $desc" >&2 + fi +done + +if [[ $DRY -eq 1 ]]; then + echo "(dry-run) would have cleaned $n entr(y/ies). Re-run as root without --dry-run." +else + echo "Cleaned $n local-DB entr(y/ies). Verify with: pacman -Dk and a quiet pacman -Q" +fi diff --git a/os updates/cachyos-repo-switch/nosignal-cachy-repo b/os updates/cachyos-repo-switch/nosignal-cachy-repo new file mode 100755 index 0000000..58b2396 --- /dev/null +++ b/os updates/cachyos-repo-switch/nosignal-cachy-repo @@ -0,0 +1,306 @@ +#!/bin/bash +# nosignal-cachy-repo — enable/disable the CachyOS pacman repositories, auto- +# selecting the best x86-64 microarchitecture tier the CPU supports (v4 > v3). +# Optimized CachyOS repos require x86-64-v3 or newer; below that this refuses. +# +# Driven by the sudoless GUI toggle: +# sudo -n /usr/local/bin/nosignal-cachy-repo {enable|disable|status|detect} [--dry-run] +# +# enable: trust keys + install mirrorlists + add repo stanzas, then run +# `pacman -Suu --noconfirm` to switch the system over to the CachyOS optimized +# builds — UNATTENDED, so a single toggle flip auto-converts (the GUI still shows +# the transaction in the floating terminal). --noconfirm auto-accepts any +# replacements/removals pacman proposes. We do NOT swap in the upstream CachyOS +# `pacman` package — it is pinned stock via --ignore pacman (keeps the local DB +# clean). enable also installs the CachyOS kernel (linux-cachyos). +# disable strips the repos, then reverts ONLY the CachyOS-built packages back to +# their stock Arch builds (-Suuy for version diffs + a targeted reinstall of the +# same-version rebuilds, identified by Packager "CachyOS …" in the local DB) and +# removes the keyring/key — a true round-trip back to stock, UNATTENDED. Touches +# only what was actually converted (nothing converted => instant, no base pull). +# A CachyOS-exclusive package with no stock equivalent is auto-skipped + REPORTED +# (not auto-removed). The CachyOS KERNEL is the exception: disable removes +# linux-cachyos, but only after confirming stock `linux` is installed so the box +# stays bootable. +set -u + +PACCONF="/etc/pacman.conf" +PACBAK="/etc/pacman.conf.nosignal-cachy.bak" +CACHY_TARBALL="https://mirror.cachyos.org/cachyos-repo.tar.xz" +MIRROR="https://mirror.cachyos.org/repo/x86_64/cachyos" +CACHY_KEY="F3B607488DB35A47" +KEYSERVER="keyserver.ubuntu.com" + +DRYRUN=0 +ARGS=() +for a in "$@"; do + if [[ "$a" == "--dry-run" ]]; then DRYRUN=1; else ARGS+=("$a"); fi +done +set -- ${ARGS+"${ARGS[@]}"} +CMD="${1:-}" + +run() { echo "+ $*"; [[ $DRYRUN -eq 1 ]] && return 0; "$@"; } + +detect_tier() { + local h; h=$(/lib/ld-linux-x86-64.so.2 --help 2>/dev/null) + if grep -q "x86-64-v4 (supported" <<<"$h"; then echo v4 + elif grep -q "x86-64-v3 (supported" <<<"$h"; then echo v3 + else echo none; fi +} + +is_enabled() { grep -qE '^\[cachyos' "$PACCONF" 2>/dev/null; } + +# --- durable stock-pacman pin ------------------------------------------------- +# `--ignore pacman` only pins pacman for THIS helper's own transaction. While the +# CachyOS repos are enabled, a routine `pacman -Syu` / `yay -Syu` / nosignal-update +# upgrades pacman to the CachyOS build, which stamps `%INSTALLED_DB%` into every +# local-DB entry it touches — stock pacman then warns about that unknown key on +# EVERY op (a flood that looks exactly like DB corruption). So while enabled we +# pin pacman in pacman.conf via IgnorePkg too; disable removes the pin. NoSignal +# ships no IgnorePkg by default, so we manage a single standalone line. +ensure_ignore_pacman() { + grep -qE '^[[:space:]]*IgnorePkg[[:space:]]*=.*\bpacman\b' "$PACCONF" && return 0 + if grep -qE '^[[:space:]]*IgnorePkg[[:space:]]*=' "$PACCONF"; then + run sed -i -E 's/^([[:space:]]*IgnorePkg[[:space:]]*=.*)$/\1 pacman/' "$PACCONF" + else + run sed -i -E '/^\[options\]/a IgnorePkg = pacman' "$PACCONF" + fi +} + +remove_ignore_pacman() { + # remove a standalone `IgnorePkg = pacman` line we added… + run sed -i -E '/^[[:space:]]*IgnorePkg[[:space:]]*=[[:space:]]*pacman[[:space:]]*$/d' "$PACCONF" + # …or drop just the `pacman` token from a shared IgnorePkg list + run sed -i -E 's/^([[:space:]]*IgnorePkg[[:space:]]*=.*[[:space:]])pacman([[:space:],]|$)/\1\2/' "$PACCONF" +} + +# Strip the CachyOS `%INSTALLED_DB%` field from the pacman LOCAL DB (left behind +# whenever a CachyOS pacman ran while enabled — e.g. a normal -Syu). The field is +# informational only; stock pacman doesn't use it and just warns. Removing it +# silences the warning flood. Each block is exactly `%INSTALLED_DB%` + one value +# line + a blank separator. Idempotent: only rewrites desc files that contain it. +strip_installed_db() { + local db="/var/lib/pacman/local" n=0 desc tmp hits + hits=$(grep -lrx '%INSTALLED_DB%' "$db"/*/desc 2>/dev/null) || true + [[ -z "$hits" ]] && { echo "Local pacman DB already clean (no %INSTALLED_DB%)."; return 0; } + while IFS= read -r desc; do + [[ -z "$desc" ]] && continue + if [[ $DRYRUN -eq 1 ]]; then echo "+ strip %INSTALLED_DB% from $desc"; n=$((n+1)); continue; fi + tmp="$desc.nsclean.$$" + if awk '/^%INSTALLED_DB%$/{s=1;next} s&&/^$/{s=0;next} s{next} {print}' "$desc" > "$tmp" \ + && chmod --reference="$desc" "$tmp" 2>/dev/null; then + mv -f "$tmp" "$desc"; n=$((n+1)) + else + rm -f "$tmp" + fi + done <<< "$hits" + echo "Stripped %INSTALLED_DB% from $n local-DB entr(y/ies); stock pacman is quiet again." +} + +need_root() { + [[ $EUID -eq 0 ]] && return 0 + echo "Error: must run as root (the toggle uses: sudo -n $0 $CMD)" >&2 + exit 1 +} + +# Fetch + extract the upstream repo tarball (for the maintained .awk stanza +# files + current keyring/mirrorlist package versions). Echoes the dir. +fetch_upstream() { + local d; d=$(mktemp -d /tmp/nosignal-cachy.XXXXXX) || return 1 + if [[ $DRYRUN -eq 1 ]]; then echo "$d"; return 0; fi + curl -fsSL "$CACHY_TARBALL" | tar -xJ -C "$d" || return 1 + echo "$d" +} + +case "$CMD" in + detect) detect_tier ;; + + status) if is_enabled; then echo enabled; exit 0; else echo disabled; exit 1; fi ;; + + enable) + need_root + if ! is_enabled; then + tier=$(detect_tier) + if [[ "$tier" == none ]]; then + echo "Error: CPU supports neither x86-64-v3 nor v4; CachyOS optimized repos" >&2 + echo " unavailable. Nothing changed." >&2 + exit 2 + fi + echo "Detected microarchitecture tier: x86-64-$tier" + up=$(fetch_upstream) || { echo "Error: could not fetch CachyOS repo tarball (network?)." >&2; exit 4; } + trap 'rm -rf "$up"' EXIT + src="$up/cachyos-repo" + + # 1) trust the CachyOS signing key + run pacman-key --recv-keys "$CACHY_KEY" --keyserver "$KEYSERVER" + run pacman-key --lsign-key "$CACHY_KEY" + + # 2) install keyring + mirrorlists (versions tracked from upstream script; + # deliberately EXCLUDE the upstream 'pacman-*' replacement — keep stock pacman) + pkgs=() + if [[ $DRYRUN -eq 1 ]]; then + pkgs=(cachyos-keyring-XX cachyos-mirrorlist-XX cachyos-v3-mirrorlist-XX cachyos-v4-mirrorlist-XX) + else + mapfile -t pkgs < <(grep -oE '[a-z0-9.+_-]+\.pkg\.tar\.zst' "$src/cachyos-repo.sh" \ + | grep -vE '^pacman-' | sort -u) + fi + urls=(); for p in "${pkgs[@]}"; do urls+=("$MIRROR/$p"); done + run pacman -U --noconfirm "${urls[@]}" + + # 3) add the repo stanzas via upstream's maintained awk (tier-appropriate). + case "$tier" in + v4) awkf="install-v4-repo.awk" ;; + v3) awkf="install-repo.awk" ;; + esac + run cp -a "$PACCONF" "$PACBAK" + run gawk -i inplace -f "$src/$awkf" "$PACCONF" + + # The upstream awk sets `Architecture = auto`, which STOCK pacman reads as + # just x86_64 — so it would REJECT every x86-64-v3/v4 package ("does not + # have a valid architecture") and the conversion would silently do nothing. + # (CachyOS's own pacman build reads `auto` as v3/v4; we keep stock pacman.) + # So set the tier arch EXPLICITLY. + case "$tier" in + v4) arch="x86_64 x86_64_v3 x86_64_v4" ;; + v3) arch="x86_64 x86_64_v3" ;; + esac + run sed -i "s/^Architecture = .*/Architecture = $arch/" "$PACCONF" + run pacman -Sy + + if [[ $DRYRUN -eq 0 ]] && ! is_enabled; then + echo "Error: CachyOS repos not present in $PACCONF after setup." >&2 + exit 3 + fi + echo "CachyOS repos added (x86-64-$tier)." + else + echo "CachyOS repos already present — upgrading to optimized builds…" + fi + + # Pull the optimized builds, UNATTENDED (--noconfirm) so a single toggle flip + # auto-converts the system without a prompt. The GUI still shows the full + # transaction in the floating terminal for visibility. NB: --noconfirm + # auto-accepts replacements/removals pacman proposes — that's the trade-off + # for hands-off conversion. + # + # --ignore pacman: the cachyos repo ships its OWN pacman build; without this + # the -Suu swaps stock pacman for it, which stamps a `%INSTALLED_DB%` key into + # every local-DB entry that stock pacman then warns about on every op. Pin + # pacman to the stock build so the DB stays clean (matches the keyring/mirror- + # list exclusion in step 2 — stock pacman is kept throughout). + # + # Make that pin DURABLE: --ignore only covers this transaction, but the repos + # stay enabled afterwards, so a later routine -Syu would pull the CachyOS pacman + # and contaminate the local DB with %INSTALLED_DB%. Pin pacman in pacman.conf + # for as long as CachyOS is enabled (disable removes the pin). + ensure_ignore_pacman + run pacman -Suu --noconfirm --ignore pacman + + # Install the CachyOS KERNEL. The repo switch above converts USERSPACE only; + # the kernel is a separate, explicit install. linux-cachyos is CachyOS's + # default (sched-ext + BORE, LTO, v3/v4-tuned). The limine-mkinitcpio-hook + # auto-generates the boot entry — no manual limine.conf editing. --needed keeps + # a re-flip idempotent; stock `linux` is left in place as a fallback entry. + run pacman -S --noconfirm --needed linux-cachyos linux-cachyos-headers + echo "CachyOS optimized builds + linux-cachyos kernel installed." + echo ">> REBOOT and pick the linux-cachyos entry at the Limine menu to run it." + ;; + + disable) + need_root + if ! is_enabled; then echo "CachyOS repos already disabled."; exit 0; fi + up=$(fetch_upstream) || up="" + [[ -n "$up" ]] && trap 'rm -rf "$up"' EXIT + + # 1) strip the CachyOS repo stanzas (pacman now sees only the stock repos) + run cp -a "$PACCONF" "$PACBAK" + if [[ -n "$up" && ( $DRYRUN -eq 1 || -f "$up/cachyos-repo/remove-repo.awk" ) ]]; then + run gawk -i inplace -f "$up/cachyos-repo/remove-repo.awk" "$PACCONF" + fi + if [[ $DRYRUN -eq 0 ]] && is_enabled; then # belt-and-braces if any remain + run sed -i '/^\[cachyos/,/^$/d' "$PACCONF" + fi + # restore the stock Architecture (enable set it to the v3/v4 tier explicitly) + run sed -i 's/^Architecture = .*/Architecture = auto/' "$PACCONF" + # drop the durable stock-pacman pin enable added (no longer needed off cachy) + remove_ignore_pacman + + # 2) TARGETED REVERT — put ONLY the CachyOS-built packages back on stock. + # -Suuy refreshes the stock DBs and downgrades/upgrades any cachy package + # whose version differs from stock (its Packager flips back to Arch). Then + # force-reinstall the REMAINING same-version CachyOS rebuilds — found by + # their Packager ("CachyOS ") recorded in the local DB — + # so we touch ONLY what was actually converted, never the whole base + # (nothing converted => nothing to do). UNATTENDED (--noconfirm). + # A CachyOS-built package with no stock equivalent (e.g. a linux-cachyos + # kernel) is auto-skipped and REPORTED — not auto-removed (could be the + # running kernel); the user decides. + run pacman -Suuy --noconfirm + + # Remove the CachyOS kernel — but ONLY once stock `linux` is confirmed present, + # so the box never loses its last bootable kernel. If stock linux is somehow + # missing, install it FIRST; if it still can't be installed, KEEP linux-cachyos. + # The limine-mkinitcpio-hook drops the boot entry automatically on removal. + # (linux-cachyos is CachyOS-exclusive, so the targeted revert below would + # otherwise just report it as an orphan and leave it installed + running.) + if [[ $DRYRUN -eq 1 ]]; then + echo "+ (if linux-cachyos present) ensure stock linux installed, then:" + echo "+ pacman -Rns --noconfirm linux-cachyos linux-cachyos-headers" + elif pacman -Qq linux-cachyos >/dev/null 2>&1; then + if ! pacman -Qq linux >/dev/null 2>&1; then + echo "Stock 'linux' missing — installing it before removing the CachyOS kernel…" + run pacman -S --noconfirm --needed linux linux-headers + fi + if pacman -Qq linux >/dev/null 2>&1; then + run pacman -Rns --noconfirm linux-cachyos linux-cachyos-headers + echo "Removed the CachyOS kernel; stock 'linux' remains as the boot entry." + echo ">> REBOOT into stock 'linux' at the Limine menu to stop running the CachyOS kernel." + else + echo "WARNING: could not ensure stock 'linux' is installed —" + echo " KEEPING linux-cachyos so the box stays bootable. Remove it manually" + echo " only after a stock kernel is installed." + fi + fi + + if [[ $DRYRUN -eq 1 ]]; then + echo "+ pacman -S --noconfirm " + echo "+ (CachyOS-exclusive pkgs are reported, not reinstalled or removed)" + else + # CachyOS-built packages (by Packager), EXCLUDING the cachy keyring/mirrorlist + # packages themselves — those are removed in step 3, not reinstalled. + mapfile -t cachy_built < <(LC_ALL=C pacman -Qi 2>/dev/null \ + | awk -F': ' '/^Name +:/{n=$2} /^Packager +:/{if ($2 ~ /CachyOS/) print n}' \ + | grep -vxE 'cachyos-(keyring|mirrorlist|v3-mirrorlist|v4-mirrorlist)') + if [[ ${#cachy_built[@]} -eq 0 ]]; then + echo "No CachyOS-built packages remain — nothing to reinstall." + else + mapfile -t avail < <(pacman -Slq) + declare -A in_stock=(); for p in "${avail[@]}"; do in_stock["$p"]=1; done + reinstall=(); orphan=() + for p in "${cachy_built[@]}"; do + if [[ -n "${in_stock[$p]:-}" ]]; then reinstall+=("$p"); else orphan+=("$p"); fi + done + [[ ${#reinstall[@]} -gt 0 ]] && run pacman -S --noconfirm "${reinstall[@]}" + if [[ ${#orphan[@]} -gt 0 ]]; then + echo + echo "NOTE: ${#orphan[@]} CachyOS-built package(s) have no stock equivalent" + echo " (CachyOS-exclusive) — left in place, review/remove manually:" + printf ' %s\n' "${orphan[@]}" + fi + fi + fi + + # 3) remove the CachyOS keyring/mirrorlists + trust key (best-effort) + run sh -c "pacman -Rns --noconfirm cachyos-keyring cachyos-mirrorlist cachyos-v3-mirrorlist cachyos-v4-mirrorlist 2>/dev/null || true" + run sh -c "pacman-key --delete $CACHY_KEY 2>/dev/null || true" + + # 4) clean up any %INSTALLED_DB% the CachyOS pacman stamped into the local DB + # while enabled (otherwise stock pacman warns about it on every op forever). + strip_installed_db + echo "CachyOS disabled; native packages reverted to stock Arch builds." + ;; + + *) + echo "Usage: $0 {detect|status|enable|disable} [--dry-run]" >&2 + exit 1 + ;; +esac diff --git a/os updates/cachyos-repo-switch/patch-servicespage.sh b/os updates/cachyos-repo-switch/patch-servicespage.sh new file mode 100755 index 0000000..5908db0 --- /dev/null +++ b/os updates/cachyos-repo-switch/patch-servicespage.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# patch-servicespage.sh — add the "CachyOS repositories" toggle to the caelestia +# Settings -> Services page. Runs as root (installer, or the pacman hook after a +# caelestia-shell upgrade reverts ServicesPage.qml). Idempotent. +# +# 1. install CachyRepoToggleRow.qml into nexus/common/ (untracked -> survives upgrades) +# 2. insert `CachyRepoToggleRow {}` after the "Smart colour scheme" ToggleRow in +# ServicesPage.qml (same anchor the sudo toggle uses; both land together) +# +# NoSignal bakes both the component and the page insert into the pinned +# nosignal-shell fork, so this patch is only for migration of older +# stock-caelestia installs (run by the nosignal-update migration). +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +NEXUS=${NEXUS:-/etc/xdg/quickshell/caelestia/modules/nexus} +COMMON="$NEXUS/common" +PAGE="$NEXUS/pages/ServicesPage.qml" + +[ -f "$PAGE" ] || { echo "caelestia ServicesPage not found at $PAGE — nothing to patch"; exit 0; } + +# 1. component type (auto-discovered via `import qs.modules.nexus.common`) +install -m 0644 "$SELF_DIR/CachyRepoToggleRow.qml" "$COMMON/CachyRepoToggleRow.qml" + +# 2. insert the row (idempotent) +if grep -q 'CachyRepoToggleRow' "$PAGE"; then + echo ":: ServicesPage already has the CachyOS toggle" + exit 0 +fi +grep -q 'smartScheme = checked' "$PAGE" || { + echo "WARNING: anchor (Smart colour scheme ToggleRow) not found in ServicesPage — skipping insert" >&2 + exit 0 +} + +cp -n "$PAGE" "$PAGE.pre-nosignal-cachy" 2>/dev/null || true +perl -0pi -e 's/(onToggled: GlobalConfig\.services\.smartScheme = checked\n\s*\})/$1\n\n CachyRepoToggleRow {\n Layout.fillWidth: true\n }/' "$PAGE" + +if grep -q 'CachyRepoToggleRow' "$PAGE"; then + echo ":: patched ServicesPage (CachyOS toggle added)" +else + echo "WARNING: ServicesPage insert did not take; restoring" >&2 + [ -f "$PAGE.pre-nosignal-cachy" ] && cp "$PAGE.pre-nosignal-cachy" "$PAGE" + exit 1 +fi diff --git a/os updates/caelestia-lock-faillock/README.md b/os updates/caelestia-lock-faillock/README.md new file mode 100755 index 0000000..c28db9f --- /dev/null +++ b/os updates/caelestia-lock-faillock/README.md @@ -0,0 +1,60 @@ +# caelestia-lock-faillock + +## Symptom +User locked the screen; the Caelestia lock screen **would not accept the +correct password**. Only recovery was a hard **reboot**. + +## Evidence (prior-boot journal, `journalctl --user -b -1`) +``` +qs[…]: PAM _pam_init_handlers: no default config other +qs[…]: pam_unix(passwd:auth): authentication failure; … user=q (×3) +qs[…]: pam_faillock(passwd:auth): Consecutive login failures for user q account temporarily locked +``` +(`qs` = Quickshell = the Caelestia shell hosting the lock.) + +## Root cause (lock-out mechanism — high confidence) +- The lock authenticates via `PamContext { config: "passwd" }` + (`modules/lock/Pam.qml:14`) → the **`passwd` PAM service**. +- On this build `/etc/pam.d/passwd` was extended to `auth include system-auth`, + and **system-auth runs `pam_faillock`**. +- Failed unlocks are counted by faillock; past the threshold it **temporarily + locks the account**, after which *every* attempt — including the correct + password — is refused until the `/run/faillock` tally clears (reboot, or + `faillock --reset`). That is exactly the reboot-to-recover the user hit. + +## The initial failure trigger +Two possible causes of the first failures (before faillock engaged): +1. **Keyboard layout** on the lock surface (this install is **GB**; if the lock + comes up US-layout, `@ " # £ \ | ~` mistype → auth fails). +2. **PAM stack init** (`no default config other`; module ordering under a + uid-1000 Quickshell process). + +## Fix +1. **Stop the lock from locking the account.** Ship a dedicated, **faillock-free** + PAM service `/etc/pam.d/caelestia` (provided: `etc-pam.d-caelestia`; + installed by `install-caelestia-lock-pam.sh`, root). +2. **Repoint the lock at it.** `modules/lock/Pam.qml`: `config: "passwd"` → + `config: "caelestia"`. + +A desktop lock must fail **closed-but-recoverable** (re-prompt), never lock the +account such that the correct password is refused. + +## Builder integration (important) +`modules/lock/Pam.qml` lives under `/etc/xdg/quickshell/caelestia`, which is +re-applied by a pacman hook after every `caelestia-shell` upgrade. The `config:` +repoint must therefore be added to NoSignal's **quickshell-patch + pacman-hook** +mechanism — the same one that re-applies the Updates page, Additions page, and +`NetworkConnection.qml` patches — as a 4th patched file. Do **not** hand-edit it +in place (a shell upgrade would revert it). Expected behaviour: lock → wrong +password ×5 (keeps re-prompting, account stays usable) → correct password +unlocks; the lock-surface layout is GB. + +## Recovery for the user, no reboot +TTY (Ctrl+Alt+F2) login → `sudo faillock --user "$USER" --reset` → back to the +session (Ctrl+Alt+F1) and unlock. `Super+Alt+L` recovers a *hung* lock but does +**not** clear a faillock lock. + +## Files +- `etc-pam.d-caelestia` — proposed faillock-free PAM service. +- `install-caelestia-lock-pam.sh` — idempotent installer (root). +- `migrations/1781431200-caelestia-lock-pam.sh` — delegates to it. diff --git a/os updates/caelestia-lock-faillock/etc-pam.d-caelestia b/os updates/caelestia-lock-faillock/etc-pam.d-caelestia new file mode 100755 index 0000000..f0c9e80 --- /dev/null +++ b/os updates/caelestia-lock-faillock/etc-pam.d-caelestia @@ -0,0 +1,20 @@ +#%PAM-1.0 +# Caelestia lock-screen authentication (NoSignal). +# +# Deliberately does NOT use pam_faillock: a desktop screen-lock must never lock +# the user out of their OWN running session. With faillock in the path (the old +# "passwd"->system-auth route), a few failed unlocks tripped a temporary account +# lock that then refused the *correct* password until /run/faillock cleared on +# reboot. See README.md. +# +# pam_unix verifies the password supplied via the PAM conversation (the lock UI) +# using the setuid unix_chkpwd helper, so it works for the uid-1000 Quickshell +# process. Add pam_systemd_home below if/when systemd-homed users are supported. +# +# F8a: deliberately NO `nullok` — a screen lock must never accept an empty +# password. (Only changes behaviour for empty-password accounts, which are +# correctly rejected; normal password auth is unaffected.) +auth required pam_unix.so +account required pam_unix.so +password required pam_unix.so +session required pam_unix.so diff --git a/os updates/caelestia-lock-faillock/install-caelestia-lock-pam.sh b/os updates/caelestia-lock-faillock/install-caelestia-lock-pam.sh new file mode 100755 index 0000000..bc170ca --- /dev/null +++ b/os updates/caelestia-lock-faillock/install-caelestia-lock-pam.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# install-caelestia-lock-pam.sh — idempotent. REQUIRES ROOT. +# +# Thin wrapper kept for back-compat (older migrations / docs reference it). +# The real work — DELIVER the faillock-free PAM service into the directory the +# lock actually reads (shellDir/assets/pam.d/caelestia) AND repoint Pam.qml — is +# done by patch-lock-pam.sh, which is also what the caelestia-shell pacman hook +# runs after every upgrade. Delegating keeps a single source of truth and avoids +# the old F1 bug where this script wrote /etc/pam.d/caelestia (a path PamContext +# never reads), leaving the lock on the faillock'd assets/pam.d/passwd service. +set -eu +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + +if [ "$(id -u)" -ne 0 ]; then + echo "must run as root" >&2 + exit 1 +fi + +exec sh "$HERE/patch-lock-pam.sh" diff --git a/os updates/caelestia-lock-faillock/patch-lock-pam.sh b/os updates/caelestia-lock-faillock/patch-lock-pam.sh new file mode 100755 index 0000000..7edb7d9 --- /dev/null +++ b/os updates/caelestia-lock-faillock/patch-lock-pam.sh @@ -0,0 +1,72 @@ +#!/bin/sh +# patch-lock-pam.sh — make the Caelestia lock screen use a faillock-free PAM +# service. Two parts, both idempotent: +# +# 1. DELIVER the faillock-free `caelestia` service into the directory the lock +# actually reads — Quickshell.shellDir + "/assets/pam.d" — i.e. +# /etc/xdg/quickshell/caelestia/assets/pam.d/caelestia. (Placing it in +# /etc/pam.d/caelestia, which PamContext never reads, makes the lock +# fall through to assets/pam.d/passwd — still faillock'd — and reject even +# the correct password.) +# 2. REPOINT the lock's PamContext at it: config: "passwd" -> "caelestia". +# +# Runs as root (via the installer, or the pacman hook after every caelestia-shell +# upgrade, which restores the shell tree under /etc/xdg — so BOTH the asset file +# and the repoint must be re-applied here each time, or an upgrade drops them). +# +# Symptom (before this fix): the lock screen rejects the CORRECT password; only a +# reboot escapes. The stock lock authenticates via PamContext { config: "passwd", +# configDirectory: shellDir + "/assets/pam.d" } -> assets/pam.d/passwd, which +# calls pam_faillock; a few failed unlocks trip a temporary account lock that +# then refuses every attempt (right password included) until /run/faillock clears. +# +# TARGET (Pam.qml) is overridable for testing the change against a copy; the +# asset destination is derived from it (shellDir = TARGET/../../..). +set -eu + +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +SRC_PAM="$HERE/etc-pam.d-caelestia" # the faillock-free service we ship + +TARGET=${TARGET:-/etc/xdg/quickshell/caelestia/modules/lock/Pam.qml} + +[ -f "$TARGET" ] || { echo "caelestia-shell lock not found at $TARGET — nothing to patch"; exit 0; } + +# shellDir = the caelestia shell root = three levels up from modules/lock/Pam.qml. +SHELLDIR=$(CDPATH= cd -- "$(dirname -- "$TARGET")/../.." && pwd) +ASSETS_PAMD="$SHELLDIR/assets/pam.d" + +# --- 1. deliver the faillock-free service where the lock reads it ------------- +if [ -f "$SRC_PAM" ]; then + install -d -m 0755 "$ASSETS_PAMD" + install -m 0644 "$SRC_PAM" "$ASSETS_PAMD/caelestia" + echo ":: installed $ASSETS_PAMD/caelestia (faillock-free lock auth)" +else + echo "WARNING: source PAM service $SRC_PAM missing — cannot deliver the lock service." >&2 +fi + +# --- 2. repoint the lock's PamContext at it ---------------------------------- +# Already pointed at our service? +if grep -q 'config:[[:space:]]*"caelestia"' "$TARGET"; then + echo ":: Pam.qml already points at the caelestia lock service" + exit 0 +fi + +# Only act if the stock "passwd" service is referenced. +if ! grep -q 'config:[[:space:]]*"passwd"' "$TARGET"; then + echo "WARNING: Pam.qml does not reference config: \"passwd\" — upstream shape changed." >&2 + echo " Leaving it untouched; review the lock PamContext for the new" >&2 + echo " service name and update the regex in $(basename "$0")." >&2 + exit 0 +fi + +cp -n "$TARGET" "$TARGET.pre-nosignal" 2>/dev/null || true + +# Repoint only the PAM service name; keep everything else as-is. +sed -i 's/config:\([[:space:]]*\)"passwd"/config:\1"caelestia"/g' "$TARGET" + +if grep -q 'config:[[:space:]]*"caelestia"' "$TARGET"; then + echo ":: patched $TARGET (lock now uses the faillock-free caelestia PAM service)" +else + echo "WARNING: patch did not apply — Pam.qml left untouched." >&2 + exit 0 +fi diff --git a/os updates/cheatsheet-keymap-path/README.md b/os updates/cheatsheet-keymap-path/README.md new file mode 100755 index 0000000..2a515c9 --- /dev/null +++ b/os updates/cheatsheet-keymap-path/README.md @@ -0,0 +1,44 @@ +# cheatsheet-keymap-path + +User-level, no root. + +## Symptom / risk +The Super+K cheatsheet is generated by `nosignal-keybinds-gen` from the keymap +markdown. Its source-resolution order is: + +``` +$1 arg → $NOSIGNAL_KEYMAP_MD → $HOME/Downloads/NoSignal-keybindings.md + → $HOME/.local/share/nosignal/NoSignal-keybindings.md +``` + +`~/Downloads` is checked **before** the canonical layer copy. Today that copy is +absent so it falls through and works — but any stale `NoSignal-keybindings.md` +left in `~/Downloads` (a common scratch location) would **silently shadow** the +real keymap, so the cheatsheet would show wrong/old keys. The `nosignal-keybinds` +header comment also still says the doc lives in `~/Downloads`. + +## Fix +- `nosignal-keybinds-gen`: drop the `~/Downloads` candidate so the canonical + `~/.local/share/nosignal/NoSignal-keybindings.md` is the only implicit source. + The explicit `$1` arg and `$NOSIGNAL_KEYMAP_MD` override are kept for dev use. +- `nosignal-keybinds`: correct the header comment to point at the canonical path. + +`fix-cheatsheet-keymap-path.sh` patches the on-system scripts idempotently +(awk for the candidate line + a sanity `sh -n` before saving; sed for the +comment), then refreshes the cached `keybinds.list`. No-op if already fixed. + +## Builder integration +Make the canonical change in the layer's `nosignal-keybinds-gen` and +`nosignal-keybinds` sources directly; this component is the equivalent patch for +an already-installed system. + +## Verify +``` +grep -n 'NoSignal-keybindings.md' "$(readlink -f "$(command -v nosignal-keybinds-gen)")" +# only the ~/.local/share/nosignal path should remain (no ~/Downloads) +nosignal-keybinds-gen | head -3 # still generates the keymap +``` + +## Files +- `fix-cheatsheet-keymap-path.sh` — idempotent patcher. +- `migrations/1781445600-cheatsheet-keymap-path.sh` — delegates to it. diff --git a/os updates/cheatsheet-keymap-path/fix-cheatsheet-keymap-path.sh b/os updates/cheatsheet-keymap-path/fix-cheatsheet-keymap-path.sh new file mode 100755 index 0000000..2ddfb1f --- /dev/null +++ b/os updates/cheatsheet-keymap-path/fix-cheatsheet-keymap-path.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# fix-cheatsheet-keymap-path.sh — idempotent. User-level (no root). +# +# The Super+K cheatsheet generator resolves its keymap doc from +# $HOME/Downloads/NoSignal-keybindings.md *before* the canonical +# $HOME/.local/share/nosignal/NoSignal-keybindings.md. A stale copy left in +# ~/Downloads would silently shadow the real keymap. The nosignal-keybinds +# header comment also still claims the doc lives in ~/Downloads. +# +# Fix: drop the ~/Downloads candidate from nosignal-keybinds-gen (the $1 arg and +# $NOSIGNAL_KEYMAP_MD env override remain for dev use), and correct the comment. +# Edits the real files behind the ~/.local/bin symlinks. Re-runnable. +set -eu + +resolve() { command -v "$1" >/dev/null 2>&1 && readlink -f -- "$(command -v "$1")"; } +GEN=$(resolve nosignal-keybinds-gen || true) +NK=$(resolve nosignal-keybinds || true) + +# 1. nosignal-keybinds-gen: remove the ~/Downloads candidate line. +if [ -n "${GEN:-}" ] && grep -q 'Downloads/NoSignal-keybindings.md' "$GEN"; then + tmp=$(mktemp) + awk ' + /for cand in "\$HOME\/Downloads\/NoSignal-keybindings\.md"/ { print " for cand in \\"; next } + { print } + ' "$GEN" > "$tmp" + # sanity: canonical path still present, Downloads gone, still parses + if grep -q '\.local/share/nosignal/NoSignal-keybindings.md' "$tmp" \ + && ! grep -q 'Downloads/NoSignal-keybindings.md' "$tmp" \ + && sh -n "$tmp" 2>/dev/null; then + cat "$tmp" > "$GEN"; echo "patched: $GEN (dropped ~/Downloads candidate)" + else + echo "WARN: gen rewrite failed sanity — left $GEN untouched" >&2 + fi + rm -f "$tmp" +else + echo "nosignal-keybinds-gen already canonical (or not found) — skipped" +fi + +# 2. nosignal-keybinds: fix the header comment path. +if [ -n "${NK:-}" ] && grep -q '~/Downloads/NoSignal-keybindings.md' "$NK"; then + sed -i 's#~/Downloads/NoSignal-keybindings.md#~/.local/share/nosignal/NoSignal-keybindings.md#g' "$NK" + echo "patched: $NK (comment path corrected)" +else + echo "nosignal-keybinds comment already correct (or not found) — skipped" +fi + +# refresh the cached cheatsheet list so the change takes effect immediately +[ -n "${GEN:-}" ] && "$GEN" > "$HOME/.local/share/nosignal/keybinds.list.tmp" 2>/dev/null \ + && mv "$HOME/.local/share/nosignal/keybinds.list.tmp" "$HOME/.local/share/nosignal/keybinds.list" 2>/dev/null || true + +echo "cheatsheet-keymap-path: ok" diff --git a/os updates/cheatsheet-tidy/README.md b/os updates/cheatsheet-tidy/README.md new file mode 100755 index 0000000..5bbe39d --- /dev/null +++ b/os updates/cheatsheet-tidy/README.md @@ -0,0 +1,54 @@ +# cheatsheet-tidy — Super+K panel shows key + action only, fitted + +Reworks the cheatsheet output so the Super+K panel stays compact. + +## Symptom + +The Super+K panel was cluttered and overflowed: a `[Section]` tag column +ate ~20 chars of every line, actions carried doc-provenance notes like +"(Omarchy key)", and lines ran up to 200 chars in a +72-char fuzzel panel — long actions were simply cut off mid-word. + +## Fix + +Full replacements of the two keybinds-help scripts (kept small on purpose — +the markdown stays the single source of truth and is untouched): + +- **`nosignal-keybinds-gen`** — output is now two columns, key + action: + - section tag column dropped (the markdown keeps its sections); + - doc-only parentheticals stripped from actions — anything matching + provenance patterns (`Omarchy`, `change N`, `(was …`, `moved`, + `NoSignal`, `dev/debug`); meaningful ones like + "(logout / shutdown / reboot)" and "(alias, old key)" stay; + - actions ellipsis-truncated to `$NOSIGNAL_KEYS_COLS` (default 98) so + every line fits the panel. +- **`nosignal-keybinds`** — fuzzel `--width` 72 → 100, in sync with the + generator budget (98 = 100 − margin). Comment in each file points at + the other. + +Result on the current keymap: 108 rows, all ≤ 98 chars, 6 lightly +ellipsis-truncated, none cut off by the panel. + +The installer also refreshes the flat layer-root copies of both scripts +so a re-run of `install-keybinds-help.sh` cannot +regress the panel. + +## Packaging + +Replace the two scripts in the keybinds-help source with +these versions — this component then becomes part of keybinds-help rather +than a permanent overlay. No package/config delta, no keybind changes. + +## Test + +1. `Super+K` → every visible line is `KEY action`, no `[Section]` + tags, no "(Omarchy key)"-style noise, no line wider than the panel. +2. Type to filter (e.g. "workspace") — still searchable. +3. Select a line → still copies the shortcut to the clipboard. +4. `nosignal-keybinds-gen | awk '{print length($0)}' | sort -rn | head -1` + ≤ 98. + +## Notes + +Cached list regenerated; max line length 98/98; +row count identical before/after (108). diff --git a/os updates/cheatsheet-tidy/install-cheatsheet-tidy.sh b/os updates/cheatsheet-tidy/install-cheatsheet-tidy.sh new file mode 100755 index 0000000..6be292b --- /dev/null +++ b/os updates/cheatsheet-tidy/install-cheatsheet-tidy.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# install-cheatsheet-tidy.sh — tidy the Super+K cheatsheet panel: key + +# action only, every line fits the panel. +# +# Ships full replacements of the keybinds-help scripts: +# - nosignal-keybinds-gen -> ~/.local/bin/ (no tag column, strips doc-only +# parentheticals, truncates actions to the panel budget) +# - nosignal-keybinds -> ~/.local/bin/ (fuzzel width 72 -> 100, kept in +# sync with the generator budget) +# +# Also refreshes the layer-root source copies when present, so a +# re-run of install-keybinds-help.sh cannot regress the panel. +# Safe to re-run (idempotent). No sudo needed. +set -eu + +SRC=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +BIN="$HOME/.local/bin" +ROOT=$(dirname -- "$SRC") # the layer root when run from the layer tree + +mkdir -p "$BIN" +install -m 0755 "$SRC/nosignal-keybinds-gen" "$BIN/nosignal-keybinds-gen" +install -m 0755 "$SRC/nosignal-keybinds" "$BIN/nosignal-keybinds" +echo ":: installed tidied cheatsheet scripts -> $BIN" + +# Keep the keybinds-help source copies in step (they live flat at the layer root). +# Check both the tree this component sits in and the installed layer location, +# so running from a handoff folder still fixes the on-system layer copies. +for d in "$ROOT" "$HOME/.local/share/nosignal"; do + for f in nosignal-keybinds-gen nosignal-keybinds; do + if [ -f "$d/$f" ] && ! cmp -s "$SRC/$f" "$d/$f"; then + install -m 0755 "$SRC/$f" "$d/$f" + echo ":: refreshed layer copy -> $d/$f" + fi + done +done + +# Refresh the cached list so the next Super+K shows the new format at once. +"$BIN/nosignal-keybinds-gen" > "$HOME/.local/share/nosignal/keybinds.list" 2>/dev/null || true + +echo "Done. Super+K now shows key + action only, fitted to the panel." diff --git a/os updates/cheatsheet-tidy/nosignal-keybinds b/os updates/cheatsheet-tidy/nosignal-keybinds new file mode 100755 index 0000000..4729aa0 --- /dev/null +++ b/os updates/cheatsheet-tidy/nosignal-keybinds @@ -0,0 +1,46 @@ +#!/bin/sh +# nosignal-keybinds — searchable on-screen keybinding cheatsheet for NoSignal. +# The keymap is NOT stored here: it is generated from the markdown doc by +# nosignal-keybinds-gen on every launch, so the cheatsheet can never drift from +# ~/Downloads/NoSignal-keybindings.md. The generated list is cached so the help +# still works if the markdown is temporarily missing. +# +# Read-only: typing filters the list; selecting a line copies that shortcut to +# the clipboard (if wl-copy is present) and never executes anything. +# +# --width 100 must stay in sync with the generator's $NOSIGNAL_KEYS_COLS default +# (98 = 100 minus margin) — the generator truncates actions to that budget so +# every line fits the panel. +set -u + +DATA_DIR="$HOME/.local/share/nosignal" +LIST="$DATA_DIR/keybinds.list" +mkdir -p "$DATA_DIR" + +# Find the generator next to this script (no PATH dependency), else fall back. +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +GEN="$SELF_DIR/nosignal-keybinds-gen" +[ -x "$GEN" ] || GEN="nosignal-keybinds-gen" + +# Regenerate from the markdown when available; otherwise keep the cached list. +if "$GEN" > "$LIST.tmp" 2>/dev/null && [ -s "$LIST.tmp" ]; then + mv "$LIST.tmp" "$LIST" +else + rm -f "$LIST.tmp" +fi + +if [ ! -s "$LIST" ]; then + command -v notify-send >/dev/null 2>&1 && + notify-send -u critical "NoSignal keys" "Cheatsheet unavailable — keymap markdown not found." + exit 1 +fi + +selection=$(fuzzel --dmenu \ + --prompt "keys> " \ + --width 100 \ + --lines 20 \ + --no-icons < "$LIST" 2>/dev/null) + +if [ -n "${selection:-}" ] && command -v wl-copy >/dev/null 2>&1; then + printf '%s' "$selection" | wl-copy +fi diff --git a/os updates/cheatsheet-tidy/nosignal-keybinds-gen b/os updates/cheatsheet-tidy/nosignal-keybinds-gen new file mode 100755 index 0000000..d7532a5 --- /dev/null +++ b/os updates/cheatsheet-tidy/nosignal-keybinds-gen @@ -0,0 +1,82 @@ +#!/bin/sh +# nosignal-keybinds-gen — parse the NoSignal keymap markdown and emit display-ready +# cheatsheet lines on stdout. The markdown is the SINGLE SOURCE OF TRUTH; the +# on-screen help (nosignal-keybinds) is always derived from it, so the two cannot +# drift. Reads every "| keys | action |" table row except the "Source map" +# section. +# +# Panel format: two columns only — key, action — sized so every +# line fits the fuzzel panel: +# - no section tag column (visual noise in the popup; the markdown keeps it) +# - doc-only parentheticals are stripped from actions: provenance notes like +# "(Omarchy key)", "(was hover-only)" belong in the doc, +# not the panel. Meaningful ones ("(logout / shutdown / reboot)") stay. +# - actions are ellipsis-truncated so no line exceeds $NOSIGNAL_KEYS_COLS +# characters (default 98 — the nosignal-keybinds fuzzel width of 100 minus +# margin). Keep the two in sync. +# +# Usage: nosignal-keybinds-gen [path-to-markdown] +# Source resolution order: $1 arg → $NOSIGNAL_KEYMAP_MD → known locations. +set -eu + +MD="${1:-${NOSIGNAL_KEYMAP_MD:-}}" +if [ -z "$MD" ]; then + for cand in "$HOME/Downloads/NoSignal-keybindings.md" \ + "$HOME/.local/share/nosignal/NoSignal-keybindings.md"; do + [ -f "$cand" ] && { MD="$cand"; break; } + done +fi +[ -n "${MD:-}" ] && [ -f "$MD" ] || { + echo "nosignal-keybinds-gen: keymap markdown not found (set \$NOSIGNAL_KEYMAP_MD)" >&2 + exit 1 +} + +awk -v cols="${NOSIGNAL_KEYS_COLS:-98}" ' +function trim(s){ gsub(/\*\*/, "", s); gsub(/^[ \t]+|[ \t]+$/, "", s); return s } + +# ---- section headings ------------------------------------------------------- +/^## / { + insrc = (substr($0, 4) ~ /^Source map/) # not keybinds: skip the section + next +} +insrc { next } + +# ---- table rows ------------------------------------------------------------- +/^\|/ { + if ($0 ~ /^[|: -]+$/) next # markdown separator row + n = split($0, c, "|") # c[1] is empty (text before first |) + keys = c[2]; gsub(/`/, "", keys); keys = trim(keys) + if (keys == "" || keys == "Keys" || keys == "File") next + desc = "" + for (i = 3; i < n; i++) { # join any remaining columns + cell = c[i]; gsub(/`/, "", cell); cell = trim(cell) + if (cell != "") desc = (desc == "") ? cell : desc " — " cell + } + + # strip doc-only parentheticals (provenance/changelog), keep the rest + out = ""; rest = desc + while (match(rest, /\([^()]*\)/)) { + pre = substr(rest, 1, RSTART - 1) + par = substr(rest, RSTART, RLENGTH) + rest = substr(rest, RSTART + RLENGTH) + if (par ~ /Omarchy|change [0-9]+|^\(was |moved |NoSignal|dev\/debug/) par = "" + out = out pre par + } + desc = out rest + gsub(/ +/, " ", desc) # collapse gaps left by stripping + gsub(/[ \t]+$/, "", desc) + sub(/ — $/, "", desc) + + KEY[++r] = keys; DSC[r] = desc + if (length(keys) > wk) wk = length(keys) +} + +END { + budget = cols - wk - 2 # action space after the key column + for (i = 1; i <= r; i++) { + d = DSC[i] + if (length(d) > budget) d = substr(d, 1, budget - 1) "…" + printf "%-*s %s\n", wk, KEY[i], d + } +} +' "$MD" diff --git a/os updates/cliamp-music/README.md b/os updates/cliamp-music/README.md new file mode 100755 index 0000000..ecf858a --- /dev/null +++ b/os updates/cliamp-music/README.md @@ -0,0 +1,57 @@ +# cliamp-music — CLIAmp as the default music player + +Adds a default music player — NoSignal previously had none. + +## What + +[CLIAmp](https://github.com/search?q=cliamp) — "a retro terminal music +player inspired by Winamp 2.x" — is what Omarchy ships for terminal +music (their `omarchy-launch-or-focus-tui cliamp` bind), and it is +prebuilt in the `[omarchy]` pacman repo NoSignal already carries +(v1.57.0 at time of writing; deps pull in ffmpeg and yt-dlp). + +- **Package:** `cliamp` from `[omarchy]` (repo install, no AUR build). +- **Default player:** `cliamp.desktop` (launches in kitty, class + `cliamp`) + `xdg-mime default` for 12 common audio types, mirroring + the style of omarchy's `install/config/mimetypes.sh` (which sets no + audio defaults itself). Opening an audio file anywhere now starts + CLIAmp. +- **Keybind:** `Super+M` → launch-or-focus CLIAmp, floating 1100×700 + (same shape as the other NoSignal TUI tools). Before this change both + `Super+M` and `Super+Shift+M` toggled the shell's music panel (an + MPRIS controller, not a player); the panel keeps `Super+Shift+M`. + Launch-or-focus is inlined (`hyprctl dispatch focuswindow … || kitty …`) + since NoSignal does not ship omarchy's helper — note `hyprctl` exits 0 + even when no window matches, so the check greps for `ok`. +- **Keymap doc:** `Super+M` row updated (the Super+K cheatsheet + regenerates from it on next open). + +## Files + +| File | Role | +|------|------| +| `install-cliamp-music.sh` | idempotent installer — package (sudo), desktop entry, MIME defaults, marked bind block, keymap doc row | +| `cliamp.desktop` | desktop entry: `kitty --class cliamp -e cliamp %F` + audio MimeType list | +| `hyprland-cliamp.conf` | `# >>> cliamp music player >>>` marked block (float rules + Super+M), appended to hypr-user.conf | + +## Packaging + +- Add `cliamp` to the package list (from `[omarchy]`). +- Bake the desktop entry, the MIME defaults (skeleton + `~/.config/mimeapps.list`), the marked block (verbatim, markers + included — the installer greps for it), and the keymap doc row. + +## Test + +1. `Super+M` → CLIAmp opens floating; `Super+M` again focuses it + (no second instance). `Super+Shift+M` still toggles the music panel. +2. Open an `.mp3`/`.flac` from the file manager → opens in CLIAmp. +3. `xdg-mime query default audio/mpeg` → `cliamp.desktop`. +4. Super+K lists `Super+M Music player — CLIAmp…`. + +## Notes + +cliamp 1.57.0-1 installed; audio/mpeg and audio/flac +default to cliamp.desktop; runtime binds confirm Super+M → +launch-or-focus CLIAmp and Super+Shift+M → music panel; keymap doc row +present. diff --git a/os updates/cliamp-music/cliamp.desktop b/os updates/cliamp-music/cliamp.desktop new file mode 100755 index 0000000..470ca22 --- /dev/null +++ b/os updates/cliamp-music/cliamp.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=CLIAmp +GenericName=Music Player +Comment=Retro terminal music player inspired by Winamp 2.x +Exec=kitty --class cliamp -e cliamp %F +Icon=audio-x-generic +Terminal=false +Categories=AudioVideo;Audio;Player; +MimeType=audio/mpeg;audio/mp4;audio/x-m4a;audio/aac;audio/flac;audio/x-flac;audio/ogg;audio/x-vorbis+ogg;audio/x-opus+ogg;audio/wav;audio/x-wav;audio/webm; diff --git a/os updates/cliamp-music/hyprland-cliamp.conf b/os updates/cliamp-music/hyprland-cliamp.conf new file mode 100755 index 0000000..2b11783 --- /dev/null +++ b/os updates/cliamp-music/hyprland-cliamp.conf @@ -0,0 +1,9 @@ +# >>> cliamp music player >>> +# CLIAmp (Winamp-style TUI, [omarchy] repo) is the default music player. +# Super+M launches/focuses it (was a second alias for the music panel toggle — +# the panel keeps Super+Shift+M). Floats like the other NoSignal TUI tools. +windowrule = float true, match:class cliamp +windowrule = size 1100 700, match:class cliamp +unbind = Super, M +bind = Super, M, exec, sh -c 'hyprctl dispatch focuswindow "class:^cliamp$" | grep -q ok || kitty --class cliamp -e cliamp' +# <<< cliamp music player <<< diff --git a/os updates/cliamp-music/install-cliamp-music.sh b/os updates/cliamp-music/install-cliamp-music.sh new file mode 100755 index 0000000..20b5b1f --- /dev/null +++ b/os updates/cliamp-music/install-cliamp-music.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# install-cliamp-music.sh — CLIAmp as the default music player. +# +# - cliamp -> installed from the [omarchy] repo (sudo) +# - cliamp.desktop -> ~/.local/share/applications (launch in kitty) +# - XDG MIME defaults -> audio/* opens CLIAmp (omarchy mimetypes style) +# - Super+M -> launch-or-focus CLIAmp, floating +# (Super+Shift+M keeps the shell music panel) +# - keymap doc -> Super+M row updated +# +# Safe to re-run (idempotent). Needs sudo only for the package install. +set -eu + +SRC=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +APPS="$HOME/.local/share/applications" +HYPRUSER="$HOME/.config/caelestia/hypr-user.conf" +KEYMAP="$HOME/.local/share/nosignal/NoSignal-keybindings.md" + +# 1. Package (prebuilt in the [omarchy] repo — no AUR build). +if ! command -v cliamp >/dev/null 2>&1; then + sudo pacman -S --needed --noconfirm cliamp +fi + +# 2. Desktop entry (ours wins over any packaged one — same desktop id). +mkdir -p "$APPS" +install -m 0644 "$SRC/cliamp.desktop" "$APPS/cliamp.desktop" +command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$APPS" || true + +# 3. Default player for audio files (mirrors omarchy's mimetypes.sh style). +if command -v xdg-mime >/dev/null 2>&1; then + for m in audio/mpeg audio/mp4 audio/x-m4a audio/aac audio/flac audio/x-flac \ + audio/ogg audio/x-vorbis+ogg audio/x-opus+ogg audio/wav audio/x-wav \ + audio/webm; do + xdg-mime default cliamp.desktop "$m" + done + echo ":: audio/* now opens CLIAmp" +fi + +# 4. Super+M bind + float rule (marked block, end of hypr-user.conf). +if [ -f "$HYPRUSER" ]; then + if grep -q '>>> cliamp music player >>>' "$HYPRUSER"; then + echo ":: hypr-user.conf already has the cliamp block" + else + printf '\n' >> "$HYPRUSER" + cat "$SRC/hyprland-cliamp.conf" >> "$HYPRUSER" + echo ":: Super+M -> CLIAmp (block appended to hypr-user.conf)" + fi + command -v hyprctl >/dev/null 2>&1 && hyprctl reload >/dev/null 2>&1 || true +else + echo "NOTE: $HYPRUSER not found — append hyprland-cliamp.conf to your Hyprland user config." +fi + +# 5. Keymap doc (single source of truth for the Super+K cheatsheet). +if [ -f "$KEYMAP" ] && ! grep -q 'CLIAmp' "$KEYMAP"; then + sed -i 's#^| `Super+M` | Music (alias) |#| `Super+M` | Music player — CLIAmp, floating (default for audio files) |#' "$KEYMAP" + grep -q 'CLIAmp' "$KEYMAP" \ + && echo ":: keymap doc updated (Super+M row)" \ + || echo "NOTE: keymap doc row not found — document Super+M manually in $KEYMAP" +fi + +echo "Done. Super+M opens CLIAmp; audio files open in it by default." diff --git a/os updates/dashboard-key/README.md b/os updates/dashboard-key/README.md new file mode 100755 index 0000000..d7b7c20 --- /dev/null +++ b/os updates/dashboard-key/README.md @@ -0,0 +1,38 @@ +# dashboard-key — Super+D opens the dashboard / calendar + +caelestia's calendar lives in its **dashboard**, which by default opens only by +**hovering the top screen edge** (`Config.dashboard.showOnHover` + +`modules/drawers/Interactions.qml`). Easy to miss and fiddly. This adds a +keybind. + +| Key | Action | +|-----|--------| +| `Super+D` | Toggle the dashboard (calendar + media/performance widgets) | +| `Super+Shift+D` | Communication special workspace (moved here from `Super+D`) | + +`Super+D` was caelestia's `$kbCommunication` (communication scratchpad +workspace). To avoid losing it, it moves to `Super+Shift+D`. The dashboard is +toggled via the drawer IPC (`qs -c caelestia ipc call drawers toggle +dashboard`) — no shell edits, no package, works offline. + +## Files + +- binds appended to `~/.config/caelestia/hypr-user.conf` (idempotent marked block) + +## Install + +```sh +sh install-dashboard-key.sh +``` + +## Builder notes — fold into omarchy-keys + +Add to `omarchy-keys/omarchy-keys-user.conf`: +``` +unbind = Super, D +bind = Super, D, exec, qs -c caelestia ipc call drawers toggle dashboard +bind = Super+Shift, D, exec, caelestia toggle communication +``` +And document `Super+D` (dashboard) + `Super+Shift+D` (communication) in +`NoSignal-keybindings.md`. The bar's `calendar_month` icon stays decorative +(making it clickable needs a shell-level change, deliberately avoided). diff --git a/os updates/dashboard-key/install-dashboard-key.sh b/os updates/dashboard-key/install-dashboard-key.sh new file mode 100755 index 0000000..7d67fd1 --- /dev/null +++ b/os updates/dashboard-key/install-dashboard-key.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# install-dashboard-key.sh — Super+D toggles the dashboard (calendar + widgets). +# Idempotent. +# +# The dashboard (which holds caelestia's calendar) was reachable only by +# hovering the top screen edge. This adds a keybind. Super+D was caelestia's +# `$kbCommunication` (communication special-workspace toggle), so that moves to +# Super+Shift+D — nothing is lost. +set -eu + +HYPRUSER="$HOME/.config/caelestia/hypr-user.conf" +MARK='dashboard-key: Super+D dashboard' + +if [ ! -f "$HYPRUSER" ]; then + echo "NOTE: $HYPRUSER not found — add the Super+D bind manually." +elif grep -qF "$MARK" "$HYPRUSER"; then + echo ":: Super+D already bound in $HYPRUSER" +else + cat >> "$HYPRUSER" <<'EOF' + +# >>> dashboard-key: Super+D dashboard >>> +# Super+D toggles the dashboard (calendar + media/perf widgets), previously +# reachable only by hovering the top edge. Super+D was the communication +# special-workspace toggle ($kbCommunication) — moved to Super+Shift+D. +unbind = Super, D +bind = Super, D, exec, qs -c caelestia ipc call drawers toggle dashboard # Dashboard / calendar +bind = Super+Shift, D, exec, caelestia toggle communication # Communication workspace (moved from Super+D) +# <<< dashboard-key: Super+D dashboard <<< +EOF + echo ":: bound Super+D -> dashboard; communication -> Super+Shift+D" +fi + +if command -v hyprctl >/dev/null 2>&1 && hyprctl version >/dev/null 2>&1; then + hyprctl reload >/dev/null 2>&1 && echo ":: reloaded Hyprland" +fi + +echo "Done. Super+D opens the calendar/dashboard; Super+Shift+D = communication." diff --git a/os updates/deckshift-login/README.md b/os updates/deckshift-login/README.md new file mode 100755 index 0000000..1a34db0 --- /dev/null +++ b/os updates/deckshift-login/README.md @@ -0,0 +1,140 @@ +# deckshift-login — full-session DeckShift gaming, password at boot + +Keeps DeckShift's gaming model exactly as designed — a **dedicated, full +gamescope session** (`gamescope-session-steam-nm`), switched by rewriting +SDDM's autologin session and restarting SDDM — but fixes the login behaviour: + +| Event | Stock DeckShift | With this component | +|-------|-----------------|---------------------| +| Cold boot | autologin, no password ever | **normal SDDM password login** | +| Desktop → Gaming (`Super+Shift+S`) | autologin into gaming session | same (one-shot autologin) | +| Gaming → Desktop (`Super+Shift+R` / Steam > Exit to Desktop) | autologin back to desktop | same (one-shot autologin) | +| SDDM crash / manual restart | autologin | password login | + +## How it works + +Stock DeckShift keeps a **permanent** `[Autologin]` drop-in +(`/etc/sddm.conf.d/zz-gaming-session.conf`, `Relogin=true`) and its +`gaming-session-switch` helper just flips the `Session=` line — so the box +never shows a password screen again. + +This component makes the autologin **one-shot**: + +- `gaming-session-switch` (drop-in replacement, same path + interface, so + DeckShift's `switch-to-gaming` / `switch-to-desktop` / `os-session-select` + and the existing sudoers rule keep working) writes the autologin drop-in + fresh on each switch (`Relogin=false`) **and arms a marker in `/run`**. +- `sddm-autologin-gate` runs as `ExecStartPre=` on `sddm.service` (systemd + drop-in): marker present → consume it, keep the autologin for that single + restart; marker absent (cold boot, crash, manual restart — `/run` is tmpfs) + → delete the autologin drop-in, password greeter shows. + +Also adds the `Super+Shift+S → /usr/local/bin/switch-to-gaming` Hyprland bind: +DeckShift appends its bind to `~/.config/hypr/bindings.conf`, an **Omarchy** +path that doesn't exist on caelestia, so on this system DeckShift silently +skips the keybind — this component adds it to +`~/.config/caelestia/hypr-user.conf` instead (replacing caelestia's +`Super+Shift+S` screenshot-freeze; region shots stay on +Print / Shift+Print / Super+Shift+Alt+S). No exit bind is needed on the +desktop side: exiting happens inside the gaming session (DeckShift's evdev +keybind monitor handles `Super+Shift+R`, and Steam > Power > Exit to Desktop +calls `/usr/lib/os-session-select`). + +## Files + +``` +gaming-session-switch -> /usr/local/bin/ (replaces DeckShift's; root 755) +switch-to-desktop -> /usr/local/bin/ (replaces DeckShift's; root 755) +sddm-autologin-gate -> /usr/local/bin/ (root 755) +deckshift-autologin-gate.conf-> /etc/systemd/system/sddm.service.d/ +os-session-select -> /usr/lib/ (DeckShift's original, restored + if something overwrote it) +install-deckshift-login.sh idempotent installer (needs sudo) +``` + +**`deckshift.sh` itself is never modified** — this component only overlays +files that deckshift.sh has already installed (same paths, same interfaces, +the existing sudoers rules keep applying). That is the contract: the ISO +mimics Omarchy's SDDM setup, stock DeckShift installs onto it unchanged, and +this overlay is applied after it. + +**Why `switch-to-desktop` is overlaid too**: DeckShift's original pkills gamescope *before* its final +`systemctl restart sddm`. Killing gamescope ends the login session, and +logind then kills the session's processes — including the script itself (it +runs inside the gaming session, launched by the keybind monitor) — so the +restart never executes. Stock DeckShift survives this because its permanent +`Relogin=true` autologin re-logs the user in anyway; with one-shot autologin +it stranded the user at the greeter. The overlay keeps all of DeckShift's +cleanup (power-profile restore, suspend unmask, Bluetooth, portal marker, +Steam shutdown) but queues the SDDM restart as a detached root job — the +same pattern DeckShift's own `os-session-select` uses, which is why Steam's +"Exit to Desktop" worked all along — and lets the service stop tear down +gamescope. + +The installer also **removes the withdrawn nested gaming-mode experiment** +(`nosignal-gaming-mode*` scripts, its launcher entry, and its +`# >>> gaming-mode (nested) >>>` bind block) if the box ever had it. + +## Install (opt-in, post-install — in this order) + +```sh +# 1. DeckShift first (gaming session, switch scripts, sudoers, keybind monitor) +sh ~/deckshift/deckshift.sh +# 2. then the login fix +sh ~/.local/share/nosignal/deckshift-login/install-deckshift-login.sh +``` + +**Re-run step 2 after any re-run of `deckshift.sh`** — deckshift.sh restores +its permanent-autologin `gaming-session-switch` and recreates the standing +autologin drop-in. + +## Test + +1. Reboot → SDDM **password** prompt (session: "Hyprland (uwsm-managed)"). +2. `Super+Shift+S` → screen flickers (VT switch + SDDM restart) → Steam Big + Picture in a full gamescope session. No password asked. +3. Steam > Power > Exit to Desktop (or `Super+Shift+R`) → back to the Hyprland + desktop. No password asked. +4. Reboot again → password prompt is back (the switch autologin didn't stick). +5. `sudo systemctl restart sddm` from the desktop → password prompt (gate + cleans up; nothing autologins outside a real switch). + +## Known quirks + +- SDDM remembers the **last session** per user, so after a reboot from inside + Gaming Mode the greeter may preselect "Gaming Mode (ChimeraOS)" — pick + "Hyprland (uwsm-managed)" from the session menu. +- The switch necessarily restarts the whole graphical session: open desktop + apps close when entering Gaming Mode. That is inherent to DeckShift's + full-session design (and why it works better for games than a nested + window: gamescope owns the display, HDR/VRR/input pass through cleanly). + +## Builder notes — how to put this in the ISO (opt-in, NOT pre-enabled) + +1. **Bake the Omarchy-style SDDM setup — REQUIRED.** DeckShift's + switching is built on SDDM; the ISO must boot to an SDDM password greeter + with `hyprland-uwsm` as the default session and **no autologin baked**. +2. Copy `deckshift-login/` into the layer (lands at + `~/.local/share/nosignal/deckshift-login/`) + the copy-list line in + `nosignal-update/install-nosignal-update.sh`: + ```sh + [ -d "$SRC/deckshift-login" ] && cp -a "$SRC/deckshift-login" "$DEST/" + ``` +3. Ship migration `1781395200-deckshift-login.sh` as-is — it is + **conditional**: it only (re-)applies where DeckShift is actually installed + (`/usr/local/bin/switch-to-gaming` exists), so `nosignal-update` keeps + opted-in boxes patched and is a no-op everywhere else. +4. Do **NOT** run deckshift.sh or this installer at build time, do **NOT** + bake the `# >>> deckshift gaming keys >>>` block into the shipped + `hypr-user.conf`, and do **NOT** bake steam/gamescope/DeckShift packages. + Gaming stays opt-in: the user runs `deckshift.sh` then this installer. +5. Prerequisites already in the base: `[multilib]` + the `omarchy-*` shims + (gaming-enablement) — deckshift.sh depends on both. +6. Drop the withdrawn `gaming-mode/` (nested) component entirely if any copy + is still in the builder tree; this component supersedes it. +7. Document in `NoSignal-keybindings.md`: `Super+Shift+S` = Gaming Mode + **only after opting in** (until then it stays caelestia's + screenshot-freeze); `Super+Shift+R` works inside Gaming Mode only. +8. Verify on a clean VM: install → password login; run deckshift.sh + this + installer → switch in/out of Gaming Mode with no password; reboot → + password login is back. diff --git a/os updates/deckshift-login/deckshift-autologin-gate.conf b/os updates/deckshift-login/deckshift-autologin-gate.conf new file mode 100755 index 0000000..caad46b --- /dev/null +++ b/os updates/deckshift-login/deckshift-autologin-gate.conf @@ -0,0 +1,7 @@ +# /etc/systemd/system/sddm.service.d/deckshift-autologin-gate.conf +# One-shot autologin gate for DeckShift session switching: keeps the SDDM +# autologin drop-in only when a desktop<->gaming switch armed it (marker in +# /run); every other SDDM start (cold boot included) shows the password +# greeter. See /usr/local/bin/sddm-autologin-gate. +[Service] +ExecStartPre=/usr/local/bin/sddm-autologin-gate diff --git a/os updates/deckshift-login/gaming-session-switch b/os updates/deckshift-login/gaming-session-switch new file mode 100755 index 0000000..5efa1bc --- /dev/null +++ b/os updates/deckshift-login/gaming-session-switch @@ -0,0 +1,49 @@ +#!/bin/bash +# gaming-session-switch — DeckShift-compatible session-switch helper, patched +# for password-at-boot. Same path + same {gaming|desktop} interface as +# DeckShift's original (the existing sudoers rule and switch-to-gaming / +# switch-to-desktop / os-session-select callers all keep working). +# +# DeckShift's original keeps a PERMANENT SDDM autologin drop-in and only flips +# its Session= line, so the machine never shows a password screen. This version +# writes the drop-in fresh on every switch and arms a one-shot marker in /run; +# sddm-autologin-gate (ExecStartPre on sddm.service) consumes the marker so the +# autologin applies to THAT restart only. Cold boots, crashes, and manual SDDM +# restarts find no marker and get the normal password greeter. +set -u + +CONF="/etc/sddm.conf.d/zz-gaming-session.conf" +MARKER="/run/deckshift-session-switch" + +if [[ $EUID -ne 0 ]]; then + echo "Error: must run as root (callers use: sudo -n $0 ...)" >&2 + exit 1 +fi + +# The desktop user comes from sudo (all DeckShift callers invoke via sudo -n). +USER_NAME="${SUDO_USER:-}" +if [[ -z "$USER_NAME" ]]; then + USER_NAME=$(loginctl list-sessions --no-legend 2>/dev/null | awk '$3 != "sddm" && $3 != "" { print $3; exit }') +fi +if [[ -z "$USER_NAME" ]]; then + echo "Error: cannot determine the user to autologin" >&2 + exit 1 +fi + +case "${1:-}" in + gaming) SESSION="gamescope-session-steam-nm" ;; + desktop) SESSION="hyprland-uwsm" ;; + *) + echo "Usage: $0 {gaming|desktop}" >&2 + exit 1 + ;; +esac + +cat > "$CONF" << EOF +[Autologin] +User=${USER_NAME} +Session=${SESSION} +Relogin=false +EOF +touch "$MARKER" +echo "Session set to: $1 mode (one-shot autologin armed)" diff --git a/os updates/deckshift-login/install-deckshift-login.sh b/os updates/deckshift-login/install-deckshift-login.sh new file mode 100755 index 0000000..ea1060a --- /dev/null +++ b/os updates/deckshift-login/install-deckshift-login.sh @@ -0,0 +1,99 @@ +#!/bin/sh +# install-deckshift-login.sh — full-session DeckShift gaming with a password +# at boot. Keeps DeckShift's design (dedicated gamescope session, switched via +# SDDM restart) but makes the autologin ONE-SHOT: it only fires for the +# desktop<->gaming switch itself; every cold boot shows the password greeter. +# +# OPT-IN, POST-INSTALL component: the ISO ships this folder in the layer but +# does NOT run it. Order on the installed system: +# 1. run deckshift.sh (installs the gaming session, switch scripts, sudoers) +# 2. run THIS script (patches the login behaviour + adds the Hyprland bind) +# Re-run this script after any re-run of deckshift.sh (deckshift.sh restores +# its permanent-autologin versions of these files). +# +# Needs sudo for: /usr/local/bin, /usr/lib, /etc/systemd, /etc/sddm.conf.d. +set -eu + +SRC=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +HYPRUSER="$HOME/.config/caelestia/hypr-user.conf" +MARK='deckshift gaming keys' + +# 0. DeckShift must already be installed. +if [ ! -x /usr/local/bin/switch-to-gaming ] || [ ! -f /usr/share/wayland-sessions/gamescope-session-steam-nm.desktop ]; then + echo "ERROR: DeckShift is not installed (run deckshift.sh first, then re-run this)." + exit 1 +fi + +# 1. Replace the session-switch helper with the one-shot-autologin version. +sudo install -m 0755 "$SRC/gaming-session-switch" /usr/local/bin/gaming-session-switch +echo ":: patched gaming-session-switch (one-shot autologin per switch)" + +# 2. Install the SDDM start gate (script + sddm.service drop-in). +sudo install -m 0755 "$SRC/sddm-autologin-gate" /usr/local/bin/sddm-autologin-gate +sudo install -d -m 0755 /etc/systemd/system/sddm.service.d +sudo install -m 0644 "$SRC/deckshift-autologin-gate.conf" /etc/systemd/system/sddm.service.d/deckshift-autologin-gate.conf +sudo systemctl daemon-reload +echo ":: installed sddm autologin gate (password greeter on every cold boot)" + +# 3. Drop the standing autologin drop-in; from now on it only exists +# transiently between "switch requested" and "SDDM restarted". +if [ -f /etc/sddm.conf.d/zz-gaming-session.conf ]; then + sudo rm -f /etc/sddm.conf.d/zz-gaming-session.conf + echo ":: removed standing SDDM autologin drop-in" +fi + +# 4. Ensure /usr/lib/os-session-select is DeckShift's handler (restores it if +# the withdrawn nested-gaming experiment overwrote it). +if ! cmp -s "$SRC/os-session-select" /usr/lib/os-session-select 2>/dev/null; then + sudo install -m 0755 "$SRC/os-session-select" /usr/lib/os-session-select + echo ":: restored DeckShift os-session-select (Steam Exit to Desktop)" +fi + +# 4b. Overlay switch-to-desktop: DeckShift's original pkills gamescope before +# its final `systemctl restart sddm`; the session teardown kills the +# script (it runs inside the gaming session) and the restart never fires — +# with one-shot autologin that strands the user at the greeter. Our +# version queues the restart as a detached root job instead (the proven +# os-session-select pattern), so Super+Shift+R returns to the desktop. +sudo install -m 0755 "$SRC/switch-to-desktop" /usr/local/bin/switch-to-desktop +echo ":: overlaid switch-to-desktop (queued SDDM restart, fixes Super+Shift+R exit)" + +# 5. Remove the withdrawn nested-gaming-mode bits if this box ever had them. +rm -f "$HOME/.local/bin/nosignal-gaming-mode" "$HOME/.local/bin/nosignal-gaming-mode-exit" \ + "$HOME/.local/share/applications/gaming-mode.desktop" +if [ -f "$HYPRUSER" ] && grep -qF 'gaming-mode (nested)' "$HYPRUSER"; then + sed -i '/# >>> gaming-mode (nested) >>>/,/# <<< gaming-mode (nested) << switch-to-gaming. The BASE config now ships a +# self-guarding Super+Shift+S bind (omarchy-keys-user.conf) that launches the +# gaming session only if DeckShift is installed and does nothing otherwise, so +# this installer no longer adds the bind itself. We only add a GUARDED bind as a +# fallback if no switch-to-gaming bind exists at all (e.g. an older base without +# it) — never the old unguarded form, which would break Super+Shift+S on a box +# where the gaming session is missing. Exit from Gaming Mode is handled INSIDE +# the session (DeckShift's Super+Shift+R monitor, or Steam > Power > Exit). +if [ ! -f "$HYPRUSER" ]; then + echo "NOTE: $HYPRUSER not found — base provides the guarded Super+Shift+S bind." +elif grep -qF 'switch-to-gaming' "$HYPRUSER"; then + echo ":: gaming keybind already present (base provides the guarded Super+Shift+S)" +else + cat >> "$HYPRUSER" << 'EOF' + +# >>> deckshift gaming keys >>> +# Super+Shift+S = Gaming Mode IF DeckShift is installed; otherwise does nothing. +# Self-guards on the gaming session file + switch-to-gaming helper. Exit happens +# inside the session (Super+Shift+R, or Steam > Power > Exit to Desktop). +unbind = Super+Shift, S +bind = Super+Shift, S, exec, sh -c '[ -x /usr/local/bin/switch-to-gaming ] && [ -f /usr/share/wayland-sessions/gamescope-session-steam-nm.desktop ] && exec /usr/local/bin/switch-to-gaming' +# <<< deckshift gaming keys <<< +EOF + echo ":: added guarded Super+Shift+S -> switch-to-gaming (base bind was absent)" +fi +if command -v hyprctl >/dev/null 2>&1 && hyprctl version >/dev/null 2>&1; then + hyprctl reload >/dev/null 2>&1 || true +fi + +echo "Done. Cold boot = password login. Super+Shift+S = Gaming Mode (full session);" +echo "Super+Shift+R or Steam > Exit to Desktop returns without a login screen." diff --git a/os updates/deckshift-login/os-session-select b/os updates/deckshift-login/os-session-select new file mode 100755 index 0000000..4e28953 --- /dev/null +++ b/os updates/deckshift-login/os-session-select @@ -0,0 +1,15 @@ +#!/bin/bash +# /usr/lib/os-session-select — Steam's "Exit to Desktop" handler. This is +# DeckShift's original version, verbatim (shipped here so the installer can +# restore it if something else overwrote it). It flips the session back to +# desktop, shuts Steam down, and restarts SDDM; with the patched +# gaming-session-switch the autologin back to the desktop is one-shot. +rm -f /tmp/.gaming-session-active +sudo -n /usr/local/bin/gaming-session-switch desktop 2>/dev/null || { + echo "Warning: Failed to update session config" +} +timeout 5 steam -shutdown 2>/dev/null || true +sleep 1 +nohup sudo -n systemctl restart sddm &>/dev/null & +disown +exit 0 diff --git a/os updates/deckshift-login/sddm-autologin-gate b/os updates/deckshift-login/sddm-autologin-gate new file mode 100755 index 0000000..a1e87b6 --- /dev/null +++ b/os updates/deckshift-login/sddm-autologin-gate @@ -0,0 +1,20 @@ +#!/bin/bash +# sddm-autologin-gate — ExecStartPre for sddm.service (one-shot autologin). +# +# A DeckShift session switch arms autologin right before restarting SDDM: +# gaming-session-switch writes the autologin drop-in AND touches the marker. +# This gate runs every time SDDM starts: +# - marker present -> a switch is in flight: consume the marker, keep the +# drop-in, SDDM autologins into the target session once; +# - marker absent -> cold boot / crash / manual restart: delete any stale +# drop-in so the normal password greeter shows. +# /run is tmpfs, so the marker can never survive a reboot. +CONF="/etc/sddm.conf.d/zz-gaming-session.conf" +MARKER="/run/deckshift-session-switch" + +if [[ -f "$MARKER" ]]; then + rm -f "$MARKER" +else + rm -f "$CONF" +fi +exit 0 diff --git a/os updates/deckshift-login/switch-to-desktop b/os updates/deckshift-login/switch-to-desktop new file mode 100755 index 0000000..1131d28 --- /dev/null +++ b/os updates/deckshift-login/switch-to-desktop @@ -0,0 +1,64 @@ +#!/bin/bash +# switch-to-desktop — deckshift-login overlay of DeckShift's original. +# +# WHY THIS OVERLAY EXISTS: the original pkilled gamescope and slept BEFORE its +# final `systemctl restart sddm`. Killing gamescope ends the login session, and +# logind then kills the session's processes — including this very script (it +# is launched by the in-session keybind monitor) — so the restart never ran. +# Stock DeckShift got away with that because its PERMANENT Relogin=true +# autologin re-logged the user in anyway; with deckshift-login's one-shot +# autologin it left the SDDM greeter instead. +# +# Fix, copied from the os-session-select path that already works: do the +# cleanup, arm the one-shot switch, shut Steam down, then queue the SDDM +# restart as a detached root job (nohup + disown). The restart job runs inside +# PID 1, so it completes even though stopping SDDM kills this script and the +# rest of the gaming session — no manual pkill of gamescope needed. +if [[ ! -f /tmp/.gaming-session-active ]]; then + exit 0 +fi +rm -f /tmp/.gaming-session-active + +# SYNCHRONOUS POWER RESTORE — done first because the trap-based restore in +# gamescope-session-nm-wrapper can be SIGKILL'd by `systemctl restart sddm` +# before it completes, leaving CPU governor / power profile stuck at +# "performance". (Verbatim from DeckShift's original.) +SAVED_STATE_FILE="$HOME/.cache/deckshift/saved-state" +if [[ -f "$SAVED_STATE_FILE" ]]; then + # shellcheck disable=SC1090 + source "$SAVED_STATE_FILE" + for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do + echo "${PRE_GAMING_CPU_GOVERNOR:-powersave}" > "$gov" 2>/dev/null + done + if command -v powerprofilesctl &>/dev/null && [[ -n "${PRE_GAMING_POWER_PROFILE:-}" ]]; then + sudo -n powerprofilesctl set "$PRE_GAMING_POWER_PROFILE" 2>/dev/null || \ + powerprofilesctl set "$PRE_GAMING_POWER_PROFILE" 2>/dev/null + fi + rm -f "$SAVED_STATE_FILE" +fi + +# Unmask suspend targets + daemon-reload (verbatim from DeckShift's original). +sudo -n systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null +sudo -n systemctl unmask --runtime sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null +sudo -n systemctl daemon-reload 2>/dev/null + +# Arm the one-shot autologin back to the desktop BEFORE anything can kill us. +sudo -n /usr/local/bin/gaming-session-switch desktop 2>/dev/null || true + +# Re-enable Bluetooth (verbatim from DeckShift's original). +sudo -n /usr/bin/rfkill unblock bluetooth 2>/dev/null || true +sudo -n /usr/bin/systemctl start bluetooth.service 2>/dev/null || true + +# Portal-recovery marker for the next Hyprland startup (from the original; +# moved earlier so it is written even if session teardown kills this script). +touch /tmp/.deckshift-just-returned 2>/dev/null || true + +# Give Steam a clean shutdown, then hand over to the SDDM restart. Stopping +# sddm.service tears down the whole gaming session (gamescope included) — the +# same mechanism the working Steam "Exit to Desktop" path relies on. +timeout 5 steam -shutdown 2>/dev/null || true +sleep 1 +sudo -n chvt 2 2>/dev/null || true +nohup sudo -n systemctl restart sddm &>/dev/null & +disown +exit 0 diff --git a/os updates/display-manager-sddm/README.md b/os updates/display-manager-sddm/README.md new file mode 100755 index 0000000..5da41c6 --- /dev/null +++ b/os updates/display-manager-sddm/README.md @@ -0,0 +1,63 @@ +# display-manager-sddm — switch greetd → SDDM (for DeckShift gaming mode) + +DeckShift's desktop↔gaming-mode switching is built on **SDDM**: `switch-to-gaming` +rewrites SDDM's config to point at the Gamescope/Steam session and restarts SDDM +to auto-log into it (and back). NoSignal ships **greetd + tuigreet**, which has no +equivalent, so this change replaces the display manager with SDDM. + +## What it does + +- Installs `sddm` (+ `xorg-server` for the reliable X11 greeter — the Hyprland + **session** stays Wayland; only SDDM's greeter uses X). +- Installs `/etc/sddm.conf.d/10-nosignal.conf` (X11 greeter, Numlock on, notes on + the preferred session). +- Disables `greetd.service`, enables `sddm.service`. **Takes effect on reboot.** + +## Critical: preserve the uwsm session + +greetd launched Hyprland via uwsm: +`uwsm start -e -D Hyprland hyprland.desktop`. The matching session entry is +**`/usr/share/wayland-sessions/hyprland-uwsm.desktop`** ("Hyprland +(uwsm-managed)"). Under SDDM, pick **that** session, not plain +`hyprland.desktop` (which runs `start-hyprland` directly and loses the +uwsm/systemd-managed session caelestia relies on). SDDM remembers the last +session per user. + +## Install (live) + +```sh +sh install-display-manager-sddm.sh # sudo; reboot afterwards +``` + +To revert: `sudo systemctl disable sddm; sudo systemctl enable greetd`. + +## Builder notes — display-manager swap (package + service delta) + +This is **not** a no-delta change. For the ISO: + +- **Packages:** add `sddm` (+ `xorg-server` if using the X11 greeter, OR `weston` + if using the Wayland greeter — pick one). Remove `greetd`, `greetd-tuigreet`, + `greetd-agreety` from the image package list (or keep as fallback). +- **Services:** enable `sddm.service` instead of `greetd.service` in the image. +- **Greeter display server (decision):** + - *X11 greeter* (this component's default): most reliable, but adds + `xorg-server` to an otherwise Wayland-only image. + - *Wayland greeter*: keeps the image X-free — set `DisplayServer=wayland` + + `[Wayland] CompositorCommand=weston --shell=kiosk` and add `weston`. +- **Session default:** ensure `hyprland-uwsm.desktop` is the preferred desktop + session (uwsm launch — matches the old greetd `--cmd`). Don't let plain + `hyprland.desktop` win. +- **Login behaviour:** greetd here did **not** autologin (tuigreet prompts). + SDDM keeps a password prompt unless `[Autologin]` is set. DeckShift configures + its own autologin/session entries on top — let it. +- **DeckShift autologin (interaction to know about):** DeckShift writes + `/etc/sddm.conf.d/zz-gaming-session.conf` with `[Autologin] User=… / + Session=hyprland-uwsm / Relogin=true`. Once DeckShift runs, this **overrides + the password prompt** — the box boots straight into the desktop, and the + desktop↔gaming switch flips `Session=` then restarts SDDM (autologin, no + greeter). That's the intended Steam-Deck-style behaviour. If the ISO wants a + password on normal boots, ship a higher-priority SDDM drop-in or adjust + DeckShift's `gaming-session-switch` to only autologin while in gaming mode. +- Pairs with gaming-enablement (multilib + omarchy-pkg-add). Together + these two make NoSignal DeckShift/Steam-Deck capable. +- The base's plymouth/limine boot splash is unaffected. diff --git a/os updates/display-manager-sddm/install-display-manager-sddm.sh b/os updates/display-manager-sddm/install-display-manager-sddm.sh new file mode 100755 index 0000000..41904e0 --- /dev/null +++ b/os updates/display-manager-sddm/install-display-manager-sddm.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# install-display-manager-sddm.sh — switch the display manager from greetd to +# SDDM, so DeckShift's SDDM-based desktop<->gaming session switching works. +# Idempotent. Needs sudo. Takes effect on next reboot. +set -eu + +SRC=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + +# 1. Install SDDM (+ xorg-server for the reliable X11 greeter; the Hyprland +# session itself stays Wayland). For an X-free image use the Wayland greeter +# instead (see sddm-10-nosignal.conf) and drop xorg-server here. +echo ":: installing sddm + xorg-server (sudo)..." +sudo pacman -S --needed --noconfirm sddm xorg-server + +# 2. Config drop-in (default desktop session = the uwsm-managed Hyprland). +sudo install -D -m 0644 "$SRC/sddm-10-nosignal.conf" /etc/sddm.conf.d/10-nosignal.conf +echo ":: installed /etc/sddm.conf.d/10-nosignal.conf" + +# 3. Swap the active display-manager service: greetd -> sddm. +if systemctl is-enabled greetd.service >/dev/null 2>&1; then + sudo systemctl disable greetd.service + echo ":: disabled greetd.service" +fi +sudo systemctl enable sddm.service +echo ":: enabled sddm.service (display-manager.service -> sddm)" + +cat <<'EOF' +Done. Reboot to log in via SDDM. + - At the SDDM session picker choose "Hyprland (uwsm-managed)" once; SDDM remembers it. + - DeckShift's switch-to-gaming / switch-to-desktop then drive SDDM to flip + between your desktop and the Gamescope/Steam session. +EOF diff --git a/os updates/display-manager-sddm/sddm-10-nosignal.conf b/os updates/display-manager-sddm/sddm-10-nosignal.conf new file mode 100755 index 0000000..b32112e --- /dev/null +++ b/os updates/display-manager-sddm/sddm-10-nosignal.conf @@ -0,0 +1,17 @@ +# NoSignal SDDM config (installed to /etc/sddm.conf.d/10-nosignal.conf). +[General] +# X11 greeter — the reliable, well-trodden path (needs xorg-server). The Hyprland +# SESSION still runs on Wayland; only SDDM's own greeter uses X here. +# For an X-free image instead, use the Wayland greeter: +# DisplayServer=wayland +# [Wayland] +# CompositorCommand=weston --shell=kiosk (adds `weston` to the image) +DisplayServer=x11 +Numlock=on + +[Users] +# The desktop session to prefer. hyprland-uwsm.desktop launches Hyprland through +# uwsm exactly like the old greetd config — keep this (NOT plain hyprland.desktop), +# or caelestia's uwsm/systemd-managed session env won't apply. +# (SDDM remembers the last-used session per user; DeckShift overrides this when +# switching to/from its Gamescope gaming session.) diff --git a/os updates/fish-to-bash/README.md b/os updates/fish-to-bash/README.md new file mode 100755 index 0000000..5224118 --- /dev/null +++ b/os updates/fish-to-bash/README.md @@ -0,0 +1,61 @@ +# NoSignal — fish → bash migration (Omarchy bash setup) + +Switches this Caelestia/NoSignal box from the fish shell to **bash, configured to +match [Omarchy](https://github.com/basecamp/omarchy)**. Self-contained so it +survives a wipe — copy this folder back with the rest of Downloads and run the +installer. + +## Install + +```sh +sh ~/Downloads/fish-to-bash/install-fish-to-bash.sh +``` + +Idempotent. Open a **new** terminal afterwards to land in bash. + +## What it does + +| Step | Effect | +|------|--------| +| Vendor `bash/` tree | Copied verbatim from Omarchy to `~/.local/share/omarchy/default/bash/` (`rc`, `shell`, `aliases`, `envs`, `init`, `functions`, `inputrc`, `completions`, `fns/*`) | +| `~/.bashrc` | Omarchy's template; sources the tree above. Existing `~/.bashrc` backed up to `~/.bashrc.pre-omarchy` | +| kitty | `~/.config/kitty/kitty.conf`: `shell fish` → `shell bash` | +| foot | `~/.config/foot/foot.ini`: `shell=fish` → `shell=bash` | +| Hyprland helpers | `wsaction.fish` & `configs.fish` replaced with bash ports (same filenames, bash shebang) so the stock keybinds keep working | + +## Why the layout matches Omarchy exactly + +The vendored files are byte-for-byte Omarchy and live at Omarchy's own path +(`~/.local/share/omarchy/...`). That means: (a) the prompt/aliases/history/ +completion behaviour is identical to Omarchy, and (b) you can refresh them from +upstream by re-copying `default/bash/` with no edits. `envs` sets +`OMARCHY_PATH` and prepends `~/.local/bin` to `PATH` (so the keybind-help +scripts resolve too). References to omarchy-only tools (`omarchy`, `mise`, +`opencode`, `tdl`, `gum`, …) are all guarded with `command -v` or are function +definitions, so missing tools are harmless until you actually call them. + +## NoSignal-specific additions (the only deviations from stock Omarchy) + +- `~/.bashrc` exports `EDITOR=nvim` if unset (Omarchy sets this in `uwsm/env`, + which we don't manage here). +- `~/.bashrc` runs `cat ~/.local/state/caelestia/sequences.txt` so the terminal + picks up Caelestia's dynamic colour scheme. + +## Notes / things that changed from the old fish config + +- **`ga` / `gd` now mean Omarchy's git-worktree helpers**, not the old fish + abbreviations (`git add .` / `git diff`). Other git shortcuts: `g`, `gcm`, + `gcam`, `gcad`. +- **direnv** was hooked in the old fish config; Omarchy's bash doesn't include + it. Add `eval "$(direnv hook bash)"` to the bottom of `~/.bashrc` if you want + it back. +- The **fish package is left installed** (it may be pulled in as a Caelestia + dependency). Nothing interactive uses it anymore. Remove it only after + confirming nothing else needs it: `sudo pacman -Rns fish`. +- The fish config dir (`~/.config/fish`, a Caelestia symlink) is left in place + but unused. + +## Requirements + +`bash`, and ideally `starship`, `zoxide`, `eza`, `fzf`, `bat` for the full +Omarchy experience (all guarded — bash works without them). diff --git a/os updates/fish-to-bash/bash/aliases b/os updates/fish-to-bash/bash/aliases new file mode 100755 index 0000000..656d145 --- /dev/null +++ b/os updates/fish-to-bash/bash/aliases @@ -0,0 +1,60 @@ +# File system +if command -v eza &> /dev/null; then + alias ls='eza -lh --group-directories-first --icons=auto' + alias lsa='ls -a' + alias lt='eza --tree --level=2 --long --icons --git' + alias lta='lt -a' +fi + +if [[ "$TERM" == "xterm-kitty" ]]; then + alias ff="fzf --preview 'case \$(file --mime-type -b {}) in image/*) kitty icat --clear --transfer-mode=memory --stdin=no --place=\${FZF_PREVIEW_COLUMNS}x\${FZF_PREVIEW_LINES}@0x0 {} ;; *) bat --style=numbers --color=always {} ;; esac'" +else + alias ff="fzf --preview 'bat --style=numbers --color=always {}'" +fi +alias eff='$EDITOR "$(ff)"' +sff() { if [ $# -eq 0 ]; then echo "Usage: sff (e.g. sff host:/tmp/)"; return 1; fi; local file; file=$(find . -type f -printf '%T@\t%p\n' | sort -rn | cut -f2- | ff) && [ -n "$file" ] && scp "$file" "$1"; } + +if command -v zoxide &> /dev/null; then + alias cd="zd" + zd() { + if (( $# == 0 )); then + builtin cd ~ || return + elif [[ -d $1 ]]; then + builtin cd "$1" || return + else + if ! z "$@"; then + echo "Error: Directory not found" + return 1 + fi + + printf "\U000F17A9 " + pwd + fi + } +fi + +open() ( + xdg-open "$@" >/dev/null 2>&1 & +) + +# Directories +alias ..='cd ..' +alias ...='cd ../..' +alias ....='cd ../../..' + +# Tools +alias c='opencode' +alias cx='printf "\033[2J\033[3J\033[H" && claude --permission-mode bypassPermissions' +alias d='docker' +alias r='rails' +alias t='tmux attach || tmux new -s Work' +alias ic='tdl c' +alias ix='tdl cx' +alias icx='tdl c cx' +n() { if [ "$#" -eq 0 ]; then command nvim . ; else command nvim "$@"; fi; } + +# Git +alias g='git' +alias gcm='git commit -m' +alias gcam='git commit -a -m' +alias gcad='git commit -a --amend' diff --git a/os updates/fish-to-bash/bash/completions b/os updates/fish-to-bash/bash/completions new file mode 100755 index 0000000..8a82713 --- /dev/null +++ b/os updates/fish-to-bash/bash/completions @@ -0,0 +1,64 @@ +_omarchy_complete() { + COMPREPLY=() + local cur="${COMP_WORDS[COMP_CWORD]}" + + local omarchy_path bin_dir + omarchy_path=$(command -v omarchy 2>/dev/null) || return 0 + bin_dir=$(dirname -- "$(readlink -f -- "$omarchy_path" 2>/dev/null || printf '%s' "$omarchy_path")") + [[ -d $bin_dir ]] || return 0 + + local prefix="omarchy" + local i part + for ((i = 1; i < COMP_CWORD; i++)); do + part="${COMP_WORDS[i]}" + [[ -z $part || $part == -* ]] && continue + prefix+="-$part" + done + + local -A seen=() + local candidates=() + local file basename rest next + + shopt -s nullglob + for file in "$bin_dir/$prefix"-*; do + [[ -f $file && -x $file ]] || continue + basename="${file##*/}" + rest="${basename#"$prefix"-}" + next="${rest%%-*}" + if [[ -n $next && -z ${seen[$next]:-} ]]; then + seen[$next]=1 + candidates+=("$next") + fi + done + shopt -u nullglob + + if (( COMP_CWORD == 1 )); then + candidates+=("commands") + fi + + if [[ ${COMP_WORDS[1]:-} == "commands" ]] && (( COMP_CWORD >= 2 )); then + candidates+=("--all" "--json" "--markdown" "--check") + fi + + if (( ${#candidates[@]} == 0 )) && [[ -x $bin_dir/$prefix ]]; then + local args enum + args=$(grep -m 1 '^# omarchy:args=<' "$bin_dir/$prefix" 2>/dev/null) + enum="${args#*<}" + enum="${enum%%>*}" + + if [[ $enum == *"|"* && $enum != *" "* ]]; then + read -r -a candidates <<<"${enum//|/ }" + fi + fi + + if (( ${#candidates[@]} > 0 )); then + local IFS=$'\n' + COMPREPLY=($(compgen -W "${candidates[*]}" -- "$cur")) + fi +} + +complete -o default -F _omarchy_complete omarchy + +# Hide individual omarchy-* binaries from initial-word command completion; +# the unified `omarchy` dispatcher is the user-facing entry point. +complete -I -A command -X 'omarchy-*' diff --git a/os updates/fish-to-bash/bash/envs b/os updates/fish-to-bash/bash/envs new file mode 100755 index 0000000..a8d01dd --- /dev/null +++ b/os updates/fish-to-bash/bash/envs @@ -0,0 +1,11 @@ +# Editor used by CLI +export SUDO_EDITOR="$EDITOR" +export BAT_THEME=ansi + +# Color man pages with bat +export MANROFFOPT="-c" +export MANPAGER="sh -c 'col -bx | bat -l man -p'" + +# Duplicated from .config/uwsm/env so SSH works too +export OMARCHY_PATH=$HOME/.local/share/omarchy +export PATH=$OMARCHY_PATH/bin:$PATH:$HOME/.local/bin diff --git a/os updates/fish-to-bash/bash/fns/compression b/os updates/fish-to-bash/bash/fns/compression new file mode 100755 index 0000000..4e8bb81 --- /dev/null +++ b/os updates/fish-to-bash/bash/fns/compression @@ -0,0 +1,3 @@ +# Compression +compress() { tar -czf "${1%/}.tar.gz" "${1%/}"; } +alias decompress="tar -xzf" diff --git a/os updates/fish-to-bash/bash/fns/drives b/os updates/fish-to-bash/bash/fns/drives new file mode 100755 index 0000000..412faab --- /dev/null +++ b/os updates/fish-to-bash/bash/fns/drives @@ -0,0 +1,59 @@ +# Write iso file to sd card +iso2sd() { + if (( $# < 1 )); then + echo "Usage: iso2sd [output_device]" + echo "Example: iso2sd ~/Downloads/ubuntu-25.04-desktop-amd64.iso /dev/sda" + return 1 + fi + + local iso="$1" + local drive="$2" + + if [[ -z $drive ]]; then + local available_sds=$(lsblk -dpno NAME | grep -E '/dev/sd') + + if [[ -z $available_sds ]]; then + echo "No SD drives found and no drive specified" + return 1 + fi + + drive=$(omarchy-drive-select "$available_sds") + + if [[ -z $drive ]]; then + echo "No drive selected" + return 1 + fi + fi + + sudo dd bs=4M status=progress oflag=sync if="$iso" of="$drive" + sudo eject "$drive" +} + +# Format an entire drive for a single partition using exFAT +format-drive() { + if (( $# != 2 )); then + echo "Usage: format-drive " + echo "Example: format-drive /dev/sda 'My Stuff'" + echo -e "\nAvailable drives:" + lsblk -d -o NAME -n | awk '{print "/dev/"$1}' + else + echo "WARNING: This will completely erase all data on $1 and label it '$2'." + read -rp "Are you sure you want to continue? (y/N): " confirm + + if [[ $confirm =~ ^[Yy]$ ]]; then + sudo wipefs -a "$1" + sudo dd if=/dev/zero of="$1" bs=1M count=100 status=progress + sudo parted -s "$1" mklabel gpt + sudo parted -s "$1" mkpart primary 1MiB 100% + sudo parted -s "$1" set 1 msftdata on + + partition="$([[ $1 == *"nvme"* ]] && echo "${1}p1" || echo "${1}1")" + sudo partprobe "$1" || true + sudo udevadm settle || true + + sudo mkfs.exfat -n "$2" "$partition" + + echo "Drive $1 formatted as exFAT and labeled '$2'." + fi + fi +} diff --git a/os updates/fish-to-bash/bash/fns/ssh-port-forwarding b/os updates/fish-to-bash/bash/fns/ssh-port-forwarding new file mode 100755 index 0000000..8267896 --- /dev/null +++ b/os updates/fish-to-bash/bash/fns/ssh-port-forwarding @@ -0,0 +1,19 @@ +fip() { + (( $# < 2 )) && echo "Usage: fip [port2] ..." && return 1 + local host="$1" + shift + for port in "$@"; do + ssh -f -N -L "$port:localhost:$port" "$host" && echo "Forwarding localhost:$port -> $host:$port" + done +} + +dip() { + (( $# == 0 )) && echo "Usage: dip [port2] ..." && return 1 + for port in "$@"; do + pkill -f "ssh.*-L $port:localhost:$port" && echo "Stopped forwarding port $port" || echo "No forwarding on port $port" + done +} + +lip() { + pgrep -af "ssh.*-L [0-9]+:localhost:[0-9]+" || echo "No active forwards" +} diff --git a/os updates/fish-to-bash/bash/fns/tmux b/os updates/fish-to-bash/bash/fns/tmux new file mode 100755 index 0000000..0144f2e --- /dev/null +++ b/os updates/fish-to-bash/bash/fns/tmux @@ -0,0 +1,97 @@ +# Create a Tmux Dev Layout with editor, ai, and terminal +# Usage: tdl [] +tdl() { + [[ -z $1 ]] && { echo "Usage: tdl []"; return 1; } + [[ -z $TMUX ]] && { echo "You must start tmux to use tdl."; return 1; } + + local current_dir="${PWD}" + local editor_pane ai_pane ai2_pane + local ai="$1" + local ai2="$2" + + # Use TMUX_PANE for the pane we're running in (stable even if active window changes) + editor_pane="$TMUX_PANE" + + # Name the current window after the base directory name + tmux rename-window -t "$editor_pane" "$(basename "$current_dir")" + + # Split window vertically - top 85%, bottom 15% (target editor pane explicitly) + tmux split-window -v -p 15 -t "$editor_pane" -c "$current_dir" + + # Split editor pane horizontally - AI on right 30% (capture new pane ID directly) + ai_pane=$(tmux split-window -h -p 30 -t "$editor_pane" -c "$current_dir" -P -F '#{pane_id}') + + # If second AI provided, split the AI pane vertically + if [[ -n $ai2 ]]; then + ai2_pane=$(tmux split-window -v -t "$ai_pane" -c "$current_dir" -P -F '#{pane_id}') + tmux send-keys -t "$ai2_pane" "$ai2" C-m + fi + + # Run ai in the right pane + tmux send-keys -t "$ai_pane" "$ai" C-m + + # Run nvim in the left pane + tmux send-keys -t "$editor_pane" "$EDITOR ." C-m + + # Select the nvim pane for focus + tmux select-pane -t "$editor_pane" +} + +# Create multiple tdl windows with one per subdirectory in the current directory +# Usage: tdlm [] +tdlm() { + [[ -z $1 ]] && { echo "Usage: tdlm []"; return 1; } + [[ -z $TMUX ]] && { echo "You must start tmux to use tdlm."; return 1; } + + local ai="$1" + local ai2="$2" + local base_dir="$PWD" + local first=true + + # Rename the session to the current directory name (replace dots/colons which tmux disallows) + tmux rename-session "$(basename "$base_dir" | tr '.:' '--')" + + for dir in "$base_dir"/*/; do + [[ -d $dir ]] || continue + local dirpath="${dir%/}" + + if $first; then + # Reuse the current window for the first project + tmux send-keys -t "$TMUX_PANE" "cd '$dirpath' && tdl $ai $ai2" C-m + first=false + else + local pane_id=$(tmux new-window -c "$dirpath" -P -F '#{pane_id}') + tmux send-keys -t "$pane_id" "tdl $ai $ai2" C-m + fi + done +} + +# Create a multi-pane swarm layout with the same command started in each pane (great for AI) +# Usage: tsl +tsl() { + [[ -z $1 || -z $2 ]] && { echo "Usage: tsl "; return 1; } + [[ -z $TMUX ]] && { echo "You must start tmux to use tsl."; return 1; } + + local count="$1" + local cmd="$2" + local current_dir="${PWD}" + local -a panes + + tmux rename-window -t "$TMUX_PANE" "$(basename "$current_dir")" + + panes+=("$TMUX_PANE") + + while (( ${#panes[@]} < count )); do + local new_pane + local split_target="${panes[-1]}" + new_pane=$(tmux split-window -h -t "$split_target" -c "$current_dir" -P -F '#{pane_id}') + panes+=("$new_pane") + tmux select-layout -t "${panes[0]}" tiled + done + + for pane in "${panes[@]}"; do + tmux send-keys -t "$pane" "$cmd" C-m + done + + tmux select-pane -t "${panes[0]}" +} diff --git a/os updates/fish-to-bash/bash/fns/transcoding b/os updates/fish-to-bash/bash/fns/transcoding new file mode 100755 index 0000000..017ddb6 --- /dev/null +++ b/os updates/fish-to-bash/bash/fns/transcoding @@ -0,0 +1,33 @@ +# Transcoding helpers have moved to omarchy-transcode. + +transcode-video-1080p() { + omarchy-transcode "$1" mp4 1080p +} + +transcode-video-4K() { + omarchy-transcode "$1" mp4 4k +} + +transcode-video-gif() { + omarchy-transcode "$1" gif 1080p +} + +img2jpg() { + omarchy-transcode "$1" jpg high +} + +img2jpg-small() { + omarchy-transcode "$1" jpg low +} + +img2jpg-medium() { + omarchy-transcode "$1" jpg medium +} + +img2jpg-large() { + omarchy-transcode "$1" jpg high +} + +img2png() { + omarchy-transcode "$1" png high +} diff --git a/os updates/fish-to-bash/bash/fns/worktrees b/os updates/fish-to-bash/bash/fns/worktrees new file mode 100755 index 0000000..a175a64 --- /dev/null +++ b/os updates/fish-to-bash/bash/fns/worktrees @@ -0,0 +1,36 @@ +# Create a new worktree and branch from within current git directory. +ga() { + if [[ -z "$1" ]]; then + echo "Usage: ga [branch name]" + return 1 + fi + + local branch="$1" + local base="$(basename "$PWD")" + local wt_path="../${base}--${branch}" + + git worktree add -b "$branch" "$wt_path" + mise trust "$wt_path" + cd "$wt_path" +} + +# Remove worktree and branch from within active worktree directory. +gd() { + if gum confirm "Remove worktree and branch?"; then + local cwd base branch root worktree + + cwd="$(pwd)" + worktree="$(basename "$cwd")" + + # split on first `--` + root="${worktree%%--*}" + branch="${worktree#*--}" + + # Protect against accidentally nuking a non-worktree directory + if [[ "$root" != "$worktree" ]]; then + cd "../$root" + git worktree remove "$cwd" --force || return 1 + git branch -D "$branch" + fi + fi +} diff --git a/os updates/fish-to-bash/bash/functions b/os updates/fish-to-bash/bash/functions new file mode 100755 index 0000000..9f26f9b --- /dev/null +++ b/os updates/fish-to-bash/bash/functions @@ -0,0 +1 @@ +for f in $OMARCHY_PATH/default/bash/fns/*; do source "$f"; done diff --git a/os updates/fish-to-bash/bash/init b/os updates/fish-to-bash/bash/init new file mode 100755 index 0000000..c3d8c97 --- /dev/null +++ b/os updates/fish-to-bash/bash/init @@ -0,0 +1,30 @@ +if command -v mise &> /dev/null; then + eval "$(mise activate bash)" +fi + +if [[ $- == *i* ]] && [[ ${TERM:-} != "dumb" ]] && command -v starship &> /dev/null; then + eval "$(starship init bash)" +fi + +if command -v zoxide &> /dev/null; then + eval "$(zoxide init bash)" +fi + +if command -v try &> /dev/null; then + try() { + unset -f try + eval "$(SHELL=/bin/bash command try init ~/Work/tries)" + try "$@" + } +fi + +if command -v fzf &> /dev/null; then + if [[ -f /usr/share/fzf/completion.bash ]]; then + source /usr/share/fzf/completion.bash + fi + if [[ -f /usr/share/fzf/key-bindings.bash ]]; then + source /usr/share/fzf/key-bindings.bash + fi +fi + +source "$OMARCHY_PATH/default/bash/completions" diff --git a/os updates/fish-to-bash/bash/inputrc b/os updates/fish-to-bash/bash/inputrc new file mode 100755 index 0000000..3b48e55 --- /dev/null +++ b/os updates/fish-to-bash/bash/inputrc @@ -0,0 +1,47 @@ +set meta-flag on +set input-meta on +set output-meta on +set convert-meta off +set completion-ignore-case on +set completion-prefix-display-length 2 +set show-all-if-ambiguous on +set show-all-if-unmodified on + +# Arrow keys match what you've typed so far against your command history +"\e[A": history-search-backward +"\e[B": history-search-forward +"\e[C": forward-char +"\e[D": backward-char + +# Immediately add a trailing slash when autocompleting symlinks to directories +set mark-symlinked-directories on + +# Do not autocomplete hidden files unless the pattern explicitly begins with a dot +set match-hidden-files off + +# Show all autocomplete results at once +set page-completions off + +# If there are more than 200 possible completions for a word, ask to show them all +set completion-query-items 200 + +# Show extra file information when completing, like `ls -F` does +set visible-stats on + +# Be more intelligent when autocompleting by also looking at the text after +# the cursor. For example, when the current line is "cd ~/src/mozil", and +# the cursor is on the "z", pressing Tab will not autocomplete it to "cd +# ~/src/mozillail", but to "cd ~/src/mozilla". (This is supported by the +# Readline used by Bash 4.) +set skip-completed-text on + +# Coloring for Bash 4 tab completions. +set colored-stats on + +# Cycle forward and backward through completion candidates (tab/shift+tab) +# (completion listing and display behavior configured above) +TAB: menu-complete +"\e[Z": menu-complete-backward + +# On first Tab, complete the common prefix before cycling candidates +set menu-complete-display-prefix on diff --git a/os updates/fish-to-bash/bash/rc b/os updates/fish-to-bash/bash/rc new file mode 100755 index 0000000..3042e42 --- /dev/null +++ b/os updates/fish-to-bash/bash/rc @@ -0,0 +1,6 @@ +source ~/.local/share/omarchy/default/bash/envs +source ~/.local/share/omarchy/default/bash/shell +source ~/.local/share/omarchy/default/bash/aliases +source ~/.local/share/omarchy/default/bash/functions +source ~/.local/share/omarchy/default/bash/init +[[ $- == *i* ]] && bind -f ~/.local/share/omarchy/default/bash/inputrc diff --git a/os updates/fish-to-bash/bash/shell b/os updates/fish-to-bash/bash/shell new file mode 100755 index 0000000..7eb4867 --- /dev/null +++ b/os updates/fish-to-bash/bash/shell @@ -0,0 +1,13 @@ +# History control +shopt -s histappend +HISTCONTROL=ignoreboth +HISTSIZE=32768 +HISTFILESIZE="${HISTSIZE}" + +# Autocompletion +if [[ ! -v BASH_COMPLETION_VERSINFO && -f /usr/share/bash-completion/bash_completion ]]; then + source /usr/share/bash-completion/bash_completion +fi + +# Ensure command hashing is off for mise +set +h diff --git a/os updates/fish-to-bash/bashrc b/os updates/fish-to-bash/bashrc new file mode 100755 index 0000000..4a2863c --- /dev/null +++ b/os updates/fish-to-bash/bashrc @@ -0,0 +1,17 @@ +# If not running interactively, don't do anything (leave this at the top of this file) +[[ $- != *i* ]] && return + +# NoSignal: default CLI editor (Omarchy normally exports this from uwsm/env). +export EDITOR="${EDITOR:-nvim}" + +# All the default Omarchy aliases and functions +# (don't mess with these directly, just overwrite them here!) +source ~/.local/share/omarchy/default/bash/rc + +# NoSignal: apply the active Caelestia colour scheme to this terminal. +[[ $- == *i* ]] && cat ~/.local/state/caelestia/sequences.txt 2>/dev/null + +# Add your own exports, aliases, and functions here. +# +# Make an alias for invoking commands you use constantly +# alias p='python' diff --git a/os updates/fish-to-bash/hypr-configs.bash b/os updates/fish-to-bash/hypr-configs.bash new file mode 100755 index 0000000..ef1aff4 --- /dev/null +++ b/os updates/fish-to-bash/hypr-configs.bash @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Ensure the NoSignal user-override files exist — bash port of configs.fish. +# Installed to ~/.config/hypr/scripts/configs.fish (the path hyprland.conf +# exec's at startup); runs as bash via this shebang. +# Usage: configs + +reload=false +dir="$1" + +[[ -d "$dir" ]] || mkdir -p "$dir" + +if [[ ! -f "$dir/hypr-vars.conf" ]]; then + touch "$dir/hypr-vars.conf" + reload=true +fi + +if [[ ! -f "$dir/hypr-user.conf" ]]; then + touch "$dir/hypr-user.conf" + reload=true +fi + +$reload && hyprctl reload diff --git a/os updates/fish-to-bash/hypr-wsaction.bash b/os updates/fish-to-bash/hypr-wsaction.bash new file mode 100755 index 0000000..dd59fa7 --- /dev/null +++ b/os updates/fish-to-bash/hypr-wsaction.bash @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Workspace dispatcher helper — bash port of Caelestia's wsaction.fish. +# Installed to ~/.config/hypr/scripts/wsaction.fish (the path the stock +# keybinds.conf calls); runs as bash via this shebang. +# Usage: wsaction [-g] + +group=0 +if [[ "$1" == "-g" ]]; then + group=1 + shift +fi + +if [[ $# -ne 2 ]]; then + echo "Wrong number of arguments. Usage: wsaction [-g] " >&2 + exit 1 +fi + +dispatcher="$1" +target="$2" +active_ws=$(hyprctl activeworkspace -j | jq -r '.id') + +if (( group )); then + # Move to the same slot within workspace group + hyprctl dispatch "$dispatcher" "$(( (target - 1) * 10 + active_ws % 10 ))" +else + # Move to workspace within the current group + hyprctl dispatch "$dispatcher" "$(( (active_ws - 1) / 10 * 10 + target ))" +fi diff --git a/os updates/fish-to-bash/install-fish-to-bash.sh b/os updates/fish-to-bash/install-fish-to-bash.sh new file mode 100755 index 0000000..0ea3a5e --- /dev/null +++ b/os updates/fish-to-bash/install-fish-to-bash.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# install-fish-to-bash.sh — migrate this NoSignal box from fish to bash, matching +# the Omarchy bash setup. Everything ships in this folder so it survives a wipe. +# +# Applies: +# 1. Vendors Omarchy's default/bash tree -> ~/.local/share/omarchy/default/bash +# 2. Installs ~/.bashrc (Omarchy template + Caelestia colour-scheme line) +# 3. Points kitty + foot at bash +# 4. Replaces the two Hyprland helper scripts with bash ports (kept at their +# .fish paths so the stock keybinds.conf / hyprland.conf keep working) +# +# Idempotent (safe to re-run). Backs up an existing ~/.bashrc once. +# Does NOT remove the fish package — see README.md. +set -eu + +SRC=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +OMARCHY_BASH="$HOME/.local/share/omarchy/default/bash" + +# 1. Vendor the Omarchy bash tree. +mkdir -p "$OMARCHY_BASH/fns" +for f in rc shell aliases envs init functions inputrc completions; do + cp "$SRC/bash/$f" "$OMARCHY_BASH/$f" +done +cp "$SRC/bash/fns/"* "$OMARCHY_BASH/fns/" +echo "vendored Omarchy bash tree -> $OMARCHY_BASH" + +# 2. Install ~/.bashrc (back up any existing one, once). +if [ -f "$HOME/.bashrc" ] && [ ! -f "$HOME/.bashrc.pre-omarchy" ]; then + cp "$HOME/.bashrc" "$HOME/.bashrc.pre-omarchy" + echo "backed up existing ~/.bashrc -> ~/.bashrc.pre-omarchy" +fi +cp "$SRC/bashrc" "$HOME/.bashrc" +echo "installed ~/.bashrc" + +# 3. Terminals -> bash. +KITTY="$HOME/.config/kitty/kitty.conf" +if [ -f "$KITTY" ]; then + if grep -q '^shell ' "$KITTY"; then + sed -i 's/^shell .*/shell bash/' "$KITTY" + else + printf '\nshell bash\n' >> "$KITTY" + fi + echo "kitty -> bash" +fi +FOOT="$HOME/.config/foot/foot.ini" +if [ -f "$FOOT" ]; then + if grep -q '^shell=' "$FOOT"; then + sed -i 's/^shell=.*/shell=bash/' "$FOOT" + else + sed -i '1i shell=bash' "$FOOT" + fi + echo "foot -> bash" +fi + +# 4. Hyprland helper scripts -> bash (keep the .fish filenames the config calls). +SCRIPTS="$HOME/.config/hypr/scripts" +if [ -d "$SCRIPTS" ]; then + install -m 0755 "$SRC/hypr-wsaction.bash" "$SCRIPTS/wsaction.fish" + install -m 0755 "$SRC/hypr-configs.bash" "$SCRIPTS/configs.fish" + echo "Hyprland helper scripts -> bash" +fi + +# 5. Reload Hyprland if running. +if command -v hyprctl >/dev/null 2>&1 && hyprctl version >/dev/null 2>&1; then + hyprctl reload >/dev/null 2>&1 && echo "reloaded Hyprland" +fi + +echo "Done. Open a NEW terminal to land in bash." diff --git a/os updates/gamemode-toggle-deckshift/README.md b/os updates/gamemode-toggle-deckshift/README.md new file mode 100644 index 0000000..61e70ad --- /dev/null +++ b/os updates/gamemode-toggle-deckshift/README.md @@ -0,0 +1,80 @@ +# gamemode-toggle-deckshift — Game Mode toggle launches DeckShift + +Companion to `gaming-key-guard` (which does the Super+Shift+S keybind). Patches +the package-owned nosignal-shell file; the canonical fix is in the fork. + +## Request +The utilities **Game Mode** button (gamepad icon) should launch the DeckShift +gaming session when DeckShift is installed, and do nothing otherwise. Previously +it ran caelestia's cosmetic Game Mode (disable Hyprland animations/blur/gaps). + +## Change +`modules/utilities/cards/Toggles.qml`, the `gameMode` `DelegateChoice`: + +before +```qml +delegate: Toggle { + icon: "gamepad" + checked: GameMode.enabled + onClicked: GameMode.enabled = !GameMode.enabled +} +``` +after (hardened) +```qml +delegate: Toggle { + id: gmTog + property bool launching: false + icon: "gamepad" + disabled: launching + onClicked: { + if (gmTog.launching) return; + gmTog.launching = true; + gmTog.internalChecked = false; // don't latch "on" — it's a launcher + relockTimer.start(); + Quickshell.execDetached(["sh", "-c", "[ -x /usr/local/bin/switch-to-gaming ] && [ -f /usr/share/wayland-sessions/gamescope-session-steam-nm.desktop ] && exec /usr/local/bin/switch-to-gaming"]); + } + Timer { id: relockTimer; interval: 5000; onTriggered: gmTog.launching = false } +} +``` +- **Kept styled as an off-toggle** (default `isToggle: true`, no `checked`) so it + keeps the muted/outline look of the other toggles. An earlier version set + `isToggle: false`, which flipped the icon to filled/accent — that was the + reported "changed colour"; reverted. `internalChecked` is reset to false on + click so the launcher never latches "on". +- **Debounced.** A fast double-click previously fired `switch-to-gaming` twice → + two `systemctl restart sddm` calls → the first restart consumed the one-shot + autologin marker and the second fell back to the password greeter (the reported + "takes you to the login screen, not gamescope"). The `launching` guard + + `disabled` + 5 s `relockTimer` ensure one launch per press. +- The `sh -c '… && … && exec switch-to-gaming'` guard mirrors the Super+Shift+S + bind and the deckshift-login install-check: nothing happens if DeckShift is absent. +- Adds `import Quickshell` (for `execDetached`; the file only had + `Quickshell.Bluetooth`). `GameMode` (from `qs.services`) is no longer referenced + here — the cosmetic Game Mode is still available via its IPC (`gameMode` target) + if a future build wants to re-expose it elsewhere. +- **Hidden when DeckShift isn't installed.** A `deckshiftProbe` Process + (`test -f /usr/share/wayland-sessions/gamescope-session-steam-nm.desktop`) sets + `root.deckshiftInstalled` at load, and the `quickToggles` filter drops the + `gameMode` entry when it's false (same pattern as the existing `vpn` special-case) + — so the tile cleanly leaves the grid (no dead button, no gap) on non-gaming + boxes, and appears once DeckShift is installed (after a shell reload). Needs + `import Quickshell.Io` for `Process`. + +## Pieces / integration +1. **`Toggles.qml`** — corrected file (drop-in for + `modules/utilities/cards/Toggles.qml` in the nosignal-shell fork). Rebuild the + package; set `NOSIGNAL_SKIP_SHELL_PATCH` for the build. +2. **`install-gamemode-toggle-deckshift.sh`** — idempotent fallback patch (backup + + overwrite). Reverted by the next nosignal-shell upgrade (which carries the fix). +3. **`1781478000-gamemode-toggle-deckshift.sh`** → `nosignal-update/migrations/` + (delegates via `$NOSIGNAL_SRC`). + +**Shell restart required:** `qs -c caelestia -n -d` disables the file watcher, so +the patched file is not hot-reloaded — restart the shell (**Ctrl+Super+Alt+R**) or +log out/in after patching. + +## Apply on this box now (needs root) +```sh +sudo sh ~/nosignal-handoff/gamemode-toggle-deckshift/install-gamemode-toggle-deckshift.sh +``` +then **Ctrl+Super+Alt+R**. diff --git a/os updates/gamemode-toggle-deckshift/Toggles.qml b/os updates/gamemode-toggle-deckshift/Toggles.qml new file mode 100644 index 0000000..f90c8c7 --- /dev/null +++ b/os updates/gamemode-toggle-deckshift/Toggles.qml @@ -0,0 +1,207 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Bluetooth +import Caelestia.Components +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus +import qs.modules.bar.popouts as BarPopouts + +StyledRect { + id: root + + required property DrawerVisibilities visibilities + required property BarPopouts.Wrapper popouts + + // NoSignal: the gamepad "Game Mode" toggle launches DeckShift gaming. Hide it + // entirely when DeckShift isn't installed (no gamescope session file) — checked + // once at load by deckshiftProbe below. + property bool deckshiftInstalled: false + + Process { + id: deckshiftProbe + running: true + command: ["test", "-f", "/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"] + onExited: (exitCode, exitStatus) => root.deckshiftInstalled = exitCode === 0 + } + + readonly property var quickToggles: { + const seenIds = new Set(); + + return Config.utilities.quickToggles.filter(item => { + if (!(item.enabled ?? true)) + return false; + + if (seenIds.has(item.id)) { + return false; + } + + if (item.id === "vpn") { + return GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); + } + + // NoSignal: drop the Game Mode tile unless DeckShift is installed. + if (item.id === "gameMode") { + return root.deckshiftInstalled; + } + + seenIds.add(item.id); + return true; + }); + } + readonly property int splitIndex: Math.ceil(quickToggles.length / 2) + readonly property bool needExtraRow: quickToggles.length > 6 + + Layout.fillWidth: true + implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased + + radius: Tokens.rounding.large + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.medium + + StyledText { + text: qsTr("Quick Toggles") + font: Tokens.font.body.medium + } + + QuickToggleRow { + model: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles + } + + QuickToggleRow { + visible: root.needExtraRow + model: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] + } + } + + component QuickToggleRow: ButtonRow { + property alias model: repeater.model + + Layout.fillWidth: true + spacing: Tokens.spacing.small + + Repeater { + id: repeater + + delegate: DelegateChooser { + role: "id" + + DelegateChoice { + roleValue: "wifi" + delegate: Toggle { + icon: "wifi" + checked: Nmcli.wifiEnabled + onClicked: Nmcli.toggleWifi() + } + } + DelegateChoice { + roleValue: "bluetooth" + delegate: Toggle { + icon: "bluetooth" + checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type + onClicked: { + const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + if (adapter) + adapter.enabled = !adapter.enabled; + } + } + } + DelegateChoice { + roleValue: "mic" + delegate: Toggle { + icon: "mic" + checked: !Audio.sourceMuted + onClicked: { + const audio = Audio.source?.audio; + if (audio) + audio.muted = !audio.muted; + } + } + } + DelegateChoice { + roleValue: "settings" + delegate: Toggle { + icon: "settings" + inactiveOnColour: Colours.palette.m3onSurfaceVariant + isToggle: false + onClicked: { + root.visibilities.utilities = false; + WindowFactory.create(); + } + } + } + DelegateChoice { + roleValue: "gameMode" + delegate: Toggle { + id: gmTog + // NoSignal: launch the DeckShift gaming session if installed; + // otherwise do nothing. (Was caelestia's cosmetic Game Mode.) + // Kept styled as an off-toggle (muted, outline icon) — the + // gamepad is a momentary LAUNCHER, not a stateful toggle. + // DEBOUNCED: a fast double-click must not fire two + // `switch-to-gaming` runs — two SDDM restarts race the + // one-shot autologin and drop you at the password greeter. + // Guard mirrors the Super+Shift+S bind + the deckshift-login + // install-check, so it no-ops if DeckShift is absent. + property bool launching: false + icon: "gamepad" + disabled: launching + onClicked: { + if (gmTog.launching) + return; + gmTog.launching = true; + gmTog.internalChecked = false; // don't latch "on" + relockTimer.start(); + Quickshell.execDetached(["sh", "-c", "[ -x /usr/local/bin/switch-to-gaming ] && [ -f /usr/share/wayland-sessions/gamescope-session-steam-nm.desktop ] && exec /usr/local/bin/switch-to-gaming"]); + } + + Timer { + id: relockTimer + interval: 5000 + onTriggered: gmTog.launching = false + } + } + } + DelegateChoice { + roleValue: "dnd" + delegate: Toggle { + icon: "notifications_off" + checked: Notifs.dnd + onClicked: Notifs.dnd = !Notifs.dnd + } + } + DelegateChoice { + roleValue: "vpn" + delegate: Toggle { + icon: "vpn_key" + checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error" + enabled: !VPN.connecting + isToggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error" + inactiveOnColour: Colours.palette.m3onSurfaceVariant + onClicked: VPN.toggle() + } + } + } + } + } + + component Toggle: IconButton { + inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + fillWidth: true + isToggle: true + isRound: true + shapeMorph: true + } +} diff --git a/os updates/gamemode-toggle-deckshift/install-gamemode-toggle-deckshift.sh b/os updates/gamemode-toggle-deckshift/install-gamemode-toggle-deckshift.sh new file mode 100755 index 0000000..cda8e51 --- /dev/null +++ b/os updates/gamemode-toggle-deckshift/install-gamemode-toggle-deckshift.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# install-gamemode-toggle-deckshift.sh — idempotent. REQUIRES ROOT (writes the +# package-owned shell file under /etc/xdg). +# +# Repoints the utilities "Game Mode" gamepad toggle to launch the DeckShift gaming +# session if DeckShift is installed; otherwise it does nothing. (Was caelestia's +# cosmetic Game Mode toggle — disable animations/blur/gaps.) See README. +# +# Toggles.qml is shipped by the `nosignal-shell` package (baked into the fork). The +# REAL fix is in the fork source — this fallback patch is for already-installed boxes +# and is reverted by the next nosignal-shell upgrade (which carries the fixed file). +# The builder bakes it into the fork and sets NOSIGNAL_SKIP_SHELL_PATCH to skip this. +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +TARGET=/etc/xdg/quickshell/caelestia/modules/utilities/cards/Toggles.qml + +if [ -n "${NOSIGNAL_SKIP_SHELL_PATCH:-}" ]; then + echo ":: skipping fallback shell patch (NOSIGNAL_SKIP_SHELL_PATCH — fork bakes the fix)" + exit 0 +fi + +[ "$(id -u)" -eq 0 ] || { echo "must run as root (writes $TARGET)" >&2; exit 1; } + +if [ ! -f "$TARGET" ]; then + echo "WARNING: $TARGET not present — nosignal-shell not installed here; nothing to patch." >&2 + exit 0 +fi + +if cmp -s "$SELF_DIR/Toggles.qml" "$TARGET"; then + echo ":: Game Mode toggle already up to date — nothing to do." + exit 0 +fi + +# preserve the ORIGINAL (pre-NoSignal) file only on the first patch, so re-running +# with a newer Toggles.qml (e.g. the hardened version) doesn't overwrite the backup +[ -f "$TARGET.prefix.bak" ] || cp -a "$TARGET" "$TARGET.prefix.bak" +install -m 0644 "$SELF_DIR/Toggles.qml" "$TARGET" +echo ":: patched $TARGET (backup: $TARGET.prefix.bak)" +echo ":: restart the shell to apply: Ctrl+Super+Alt+R, or log out/in." diff --git a/os updates/gaming-enablement/README.md b/os updates/gaming-enablement/README.md new file mode 100755 index 0000000..c98589a --- /dev/null +++ b/os updates/gaming-enablement/README.md @@ -0,0 +1,55 @@ +# gaming-enablement — multilib repo + omarchy-pkg-add shim + +Makes Omarchy-targeted Steam/gaming install scripts (e.g. **DeckShift**) work on +NoSignal. Two gaps were blocking them: + +1. **`[multilib]` was disabled** in `/etc/pacman.conf` — the 32-bit repo every + `lib32-*` package (Steam, Wine, gaming libs) lives in. Without it, pacman/yay + can't see those packages at all. +2. **`omarchy-pkg-add` doesn't exist** on NoSignal — it ships `yay`/Shelly, not the + Omarchy CLI helpers, so scripts die at `omarchy-pkg-add: command not found`. + +## What it does + +- Uncomments the `[multilib]` block in `/etc/pacman.conf` (leaving + `[multilib-testing]` alone) and runs `pacman -Sy`. +- Installs **Omarchy CLI shims** → `~/.local/bin` (NoSignal ships yay + the + caelestia launcher, not the Omarchy helpers DeckShift expects): + + | Shim | NoSignal behaviour | + |------|------------------| + | `omarchy-pkg-add` | `yay -S --needed --noconfirm "$@"` (idempotent installs) | + | `omarchy-install-gaming-steam` | `yay -S steam` + best-effort first launch (needs multilib) | + | `omarchy-hw-nvidia-gsp` | exit 0 if an NVIDIA GPU is present (modern/GSP branch) | + | `omarchy-hw-nvidia-without-gsp` | exit 1 (defaults to modern branch; see note) | + | `omarchy-restart-walker` | no Walker here — refresh desktop DB, succeed | + + The NVIDIA shims default any present card to the modern (`nvidia-utils`) + branch; pre-Turing cards need `nvidia-580xx-*` by hand. On this AMD+Intel box + both correctly report "no NVIDIA". + +## Install + +```sh +sh install-gaming-enablement.sh # needs sudo for the pacman.conf edit + sync +``` + +After this, `./deckshift.sh` (and similar) find their deps and proceed. Note: +DeckShift also *optionally* calls `omarchy-install-gaming-steam`, +`omarchy-hw-nvidia-gsp`, `omarchy-restart-walker` — those are `command -v`-guarded +in the script, so they're skipped harmlessly (only `omarchy-pkg-add` was fatal). + +## Builder notes + +For a gaming-capable ISO: +- **Enable `[multilib]` in the image's `/etc/pacman.conf` by default** (uncomment + the two lines) and sync in the build. +- **Ship `omarchy-pkg-add`** in `~/.local/bin` (or `/usr/local/bin`). Consider + shipping the other commonly-referenced Omarchy helpers + (`omarchy-install-gaming-steam`, `omarchy-hw-nvidia-*`, `omarchy-restart-walker`) + as shims if broad Omarchy-script compatibility is wanted. +- This pairs naturally with shipping `gamemode` / `lib32-*` Steam deps in the + base if gaming is a first-class use case. + +This is a **package/repo-config delta** (unlike the other changes) — it touches +the package list / pacman.conf rather than dropping a self-contained component. diff --git a/os updates/gaming-enablement/install-gaming-enablement.sh b/os updates/gaming-enablement/install-gaming-enablement.sh new file mode 100755 index 0000000..49bcf56 --- /dev/null +++ b/os updates/gaming-enablement/install-gaming-enablement.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# install-gaming-enablement.sh — enable the multilib repo + install the +# omarchy-pkg-add shim, so Omarchy-targeted Steam/gaming install scripts (e.g. +# DeckShift) work on NoSignal. Idempotent. Needs sudo for the pacman.conf edit and +# database sync (it will prompt). +set -eu + +SRC=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +BIN="$HOME/.local/bin" + +# 1. Enable [multilib] — the 32-bit repo every lib32-* package lives in. +if pacman-conf --repo-list 2>/dev/null | grep -qx multilib; then + echo ":: [multilib] already enabled" +else + echo ":: enabling [multilib] in /etc/pacman.conf (sudo)..." + # Uncomment ONLY the [multilib] block (leaves [multilib-testing] alone). + sudo sed -i '/^#\[multilib\]/,/^#Include/ s/^#//' /etc/pacman.conf + sudo pacman -Sy + echo ":: [multilib] enabled and databases synced" +fi + +# 2. Omarchy CLI shims (NoSignal ships yay/caelestia, not the Omarchy helpers). +mkdir -p "$BIN" +for shim in omarchy-pkg-add omarchy-install-gaming-steam \ + omarchy-hw-nvidia-gsp omarchy-hw-nvidia-without-gsp \ + omarchy-restart-walker; do + install -m 0755 "$SRC/$shim" "$BIN/$shim" + echo ":: installed $shim -> $BIN/$shim" +done + +echo "Done. multilib + Omarchy shims ready — re-run your gaming installer." diff --git a/os updates/gaming-enablement/omarchy-hw-nvidia-gsp b/os updates/gaming-enablement/omarchy-hw-nvidia-gsp new file mode 100755 index 0000000..d885f8b --- /dev/null +++ b/os updates/gaming-enablement/omarchy-hw-nvidia-gsp @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# omarchy-hw-nvidia-gsp — NoSignal shim. Exit 0 if an NVIDIA GPU is present (and +# thus should use the modern, GSP-firmware driver branch: nvidia-utils). Exit 1 +# if no NVIDIA GPU. DeckShift only calls this when it has already detected an +# NVIDIA card, to choose modern vs legacy. +# +# Limitation: this defaults any present NVIDIA card to the modern/GSP branch +# (correct for Turing/RTX-20 and newer, ~2018+). It does not distinguish very +# old pre-Turing cards — see omarchy-hw-nvidia-without-gsp. +if lspci -d 10de:: 2>/dev/null | grep -qiE 'VGA|3D|Display'; then + exit 0 +fi +exit 1 diff --git a/os updates/gaming-enablement/omarchy-hw-nvidia-without-gsp b/os updates/gaming-enablement/omarchy-hw-nvidia-without-gsp new file mode 100755 index 0000000..572a6ca --- /dev/null +++ b/os updates/gaming-enablement/omarchy-hw-nvidia-without-gsp @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# omarchy-hw-nvidia-without-gsp — NoSignal shim. Exit 0 only for legacy pre-GSP +# NVIDIA (Maxwell/Pascal/Volta → the nvidia-580xx driver branch). +# +# This shim cannot reliably tell GPU generations apart, so it defaults to the +# modern branch (always exit 1; omarchy-hw-nvidia-gsp handles present cards). If +# you actually run a pre-Turing NVIDIA card, install nvidia-580xx-utils / +# nvidia-580xx-dkms by hand. +exit 1 diff --git a/os updates/gaming-enablement/omarchy-install-gaming-steam b/os updates/gaming-enablement/omarchy-install-gaming-steam new file mode 100755 index 0000000..4d671f0 --- /dev/null +++ b/os updates/gaming-enablement/omarchy-install-gaming-steam @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# omarchy-install-gaming-steam — NoSignal shim for Omarchy's Steam bootstrap. +# +# Installs Steam (yay resolves the lib32 GPU driver deps) and best-effort +# launches it once for first-run setup, mirroring Omarchy's behaviour. Requires +# the [multilib] repo enabled (steam is a multilib package) — see +# install-gaming-enablement.sh. +set -euo pipefail + +yay -S --needed --noconfirm steam + +# Best-effort first launch (non-fatal), like Omarchy's bootstrap. +if command -v steam >/dev/null 2>&1; then + setsid -f steam >/dev/null 2>&1 || true +fi diff --git a/os updates/gaming-enablement/omarchy-pkg-add b/os updates/gaming-enablement/omarchy-pkg-add new file mode 100755 index 0000000..af01190 --- /dev/null +++ b/os updates/gaming-enablement/omarchy-pkg-add @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# omarchy-pkg-add — NoSignal shim for Omarchy's helper of the same name. +# +# NoSignal ships yay (and Shelly) instead of the Omarchy CLI helpers, so +# Omarchy-targeted install scripts (e.g. the DeckShift gaming installer) fail at +# `omarchy-pkg-add: command not found`. This provides the same idempotent +# package-install behaviour via yay. +exec yay -S --needed --noconfirm "$@" diff --git a/os updates/gaming-enablement/omarchy-restart-walker b/os updates/gaming-enablement/omarchy-restart-walker new file mode 100755 index 0000000..47580fd --- /dev/null +++ b/os updates/gaming-enablement/omarchy-restart-walker @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# omarchy-restart-walker — NoSignal shim. NoSignal uses the caelestia launcher +# (Quickshell), not Omarchy's Walker/Elephant. There is no Walker service to +# restart, so just refresh the desktop-entry database (so newly-added .desktop +# files, e.g. DeckShift's, show up) and succeed. +update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true +exit 0 diff --git a/os updates/hyprland-keybinds-help.conf b/os updates/hyprland-keybinds-help.conf new file mode 100755 index 0000000..054f746 --- /dev/null +++ b/os updates/hyprland-keybinds-help.conf @@ -0,0 +1,5 @@ +# NoSignal — searchable on-screen keybinding cheatsheet (Super+/ or Super+F1). +# Append/source this into ~/.config/caelestia/hypr-user.conf. +# Absolute path so the bind works regardless of the session PATH. +bind = Super, Slash, exec, ~/.local/bin/nosignal-keybinds +bind = Super, F1, exec, ~/.local/bin/nosignal-keybinds diff --git a/os updates/install-keybinds-help.sh b/os updates/install-keybinds-help.sh new file mode 100755 index 0000000..c65882e --- /dev/null +++ b/os updates/install-keybinds-help.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# install-keybinds-help.sh — install the NoSignal on-screen keybinding cheatsheet. +# +# This component lives entirely in this folder (Downloads) so it survives a +# system wipe. Run this after rebuilding to put the pieces back in place: +# - nosignal-keybinds-gen, nosignal-keybinds -> ~/.local/bin/ +# - NoSignal-keybindings.md (source of truth) -> ~/.local/share/nosignal/ +# - Super+/ and Super+F1 binds -> ~/.config/caelestia/hypr-user.conf +# +# Safe to re-run (idempotent). Deps: awk, fuzzel, optional wl-copy. +set -eu + +SRC=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +BIN="$HOME/.local/bin" +SHARE="$HOME/.local/share/nosignal" +HYPRUSER="$HOME/.config/caelestia/hypr-user.conf" + +mkdir -p "$BIN" "$SHARE" + +install -m 0755 "$SRC/nosignal-keybinds-gen" "$BIN/nosignal-keybinds-gen" +install -m 0755 "$SRC/nosignal-keybinds" "$BIN/nosignal-keybinds" +echo "installed scripts -> $BIN" + +# Canonical copy of the single source of truth, so the help works even if the +# Downloads doc is later moved/removed. +if [ -f "$SRC/NoSignal-keybindings.md" ]; then + cp "$SRC/NoSignal-keybindings.md" "$SHARE/NoSignal-keybindings.md" + echo "installed keymap doc -> $SHARE/NoSignal-keybindings.md" +fi + +# Add the binds, only if not already present. +if [ -f "$HYPRUSER" ]; then + if grep -q 'nosignal-keybinds' "$HYPRUSER"; then + echo "binds already present in $HYPRUSER" + else + printf '\n' >> "$HYPRUSER" + cat "$SRC/hyprland-keybinds-help.conf" >> "$HYPRUSER" + echo "appended binds -> $HYPRUSER" + fi +else + echo "NOTE: $HYPRUSER not found — add the lines from hyprland-keybinds-help.conf to your Hyprland user config." +fi + +# PATH sanity. +case ":$PATH:" in + *":$BIN:"*) ;; + *) echo "NOTE: $BIN is not on PATH — add it (e.g. in ~/.bash_profile)." ;; +esac + +# Apply immediately if Hyprland is running. +if command -v hyprctl >/dev/null 2>&1 && hyprctl version >/dev/null 2>&1; then + hyprctl reload >/dev/null 2>&1 && echo "reloaded Hyprland" +fi + +echo "Done. Press Super+/ to open the cheatsheet." diff --git a/os updates/kernel-reboot-notify/95-nosignal-kernel-reboot.hook b/os updates/kernel-reboot-notify/95-nosignal-kernel-reboot.hook new file mode 100755 index 0000000..10a56d1 --- /dev/null +++ b/os updates/kernel-reboot-notify/95-nosignal-kernel-reboot.hook @@ -0,0 +1,15 @@ +# NoSignal: warn (in pacman/yay output) when a kernel image changes, so +# out-of-band `pacman -Syu` / `yay` updates also remind the user to reboot. +# The desktop notification is handled by nosignal-reboot-check (end of +# nosignal-update / at login); this hook covers the terminal case. +[Trigger] +Type = Path +Operation = Install +Operation = Upgrade +Operation = Remove +Target = usr/lib/modules/*/vmlinuz + +[Action] +Description = NoSignal: kernel changed — a reboot is required to load it... +When = PostTransaction +Exec = /bin/sh -c 'printf "\n>> NoSignal: kernel changed — REBOOT REQUIRED to load the new kernel.\n>> At the Limine menu, boot the UKI entry.\n\n"' diff --git a/os updates/kernel-reboot-notify/README.md b/os updates/kernel-reboot-notify/README.md new file mode 100755 index 0000000..e57935d --- /dev/null +++ b/os updates/kernel-reboot-notify/README.md @@ -0,0 +1,51 @@ +# kernel-reboot-notify + +## Symptom / gap +When `nosignal-update` (or any `pacman -Syu`/`yay`) installs a **new kernel**, +nothing tells the user to reboot. The new kernel + modules are written to disk, +but the **old kernel keeps running** — and its `/usr/lib/modules/` dir +has been replaced, so the live session can no longer load matching modules +(USB, filesystems, etc. can fail to load on demand). On NoSignal this also means +the freshly-built UKI isn't booted yet. The user only finds out when something +breaks — or, with **F4**, when they reboot and the default entry drops to a TTY. + +## Fix +1. **`nosignal-reboot-check`** (→ `~/.local/bin`): if the running kernel's + modules dir is gone (the update replaced it), it writes a stamp + (`$XDG_STATE_HOME/nosignal/reboot-required`), prints a yellow banner, and + raises a **critical desktop notification** ("Kernel updated — reboot to load + it (X → Y)"). It clears the stamp once the running kernel matches disk again. +2. **`95-nosignal-kernel-reboot.hook`** (→ `/etc/pacman.d/hooks`): a PostTransaction + Path hook on `usr/lib/modules/*/vmlinuz` that prints a reboot reminder in the + pacman/yay output — so updates done outside `nosignal-update` warn too. +3. **`nosignal-update` wiring**: calls `nosignal-reboot-check` as its last step, + so an interactive update ends with the reminder + notification. + +The installer (`install-kernel-reboot-notify.sh`) does all three idempotently. +The pacman-hook step needs root. + +### Optional (builder): re-warn at login until rebooted +Add `nosignal-reboot-check` to an `exec-once` (e.g. alongside `nosignal-welcome` +in `hypr-user.conf`) so a pending reboot is re-surfaced each login while the +stamp exists. Not included by default to avoid touching `hypr-user.conf` here. + +## Builder integration +Ship `nosignal-reboot-check` in the layer's bin, ship the pacman hook in the +ISO, and add the one-line call to the canonical `nosignal-update` (the installer +shows the exact insertion). Pairs with **F4**: until F4 is fixed the reboot +notice tells the user to pick the UKI entry. + +## Verify +``` +# simulate (don't actually remove modules): the helper keys off +# /usr/lib/modules/$(uname -r) — present now, so it's a no-op: +nosignal-reboot-check; echo "exit=$?" +# after a real kernel update it prints the banner + notifies and stamps +# ~/.local/state/nosignal/reboot-required +``` + +## Files +- `nosignal-reboot-check` — detector + notifier. +- `95-nosignal-kernel-reboot.hook` — pacman PostTransaction reminder. +- `install-kernel-reboot-notify.sh` — idempotent installer (hook step needs root). +- `migrations/1781438400-kernel-reboot-notify.sh` — delegates to it. diff --git a/os updates/kernel-reboot-notify/install-kernel-reboot-notify.sh b/os updates/kernel-reboot-notify/install-kernel-reboot-notify.sh new file mode 100755 index 0000000..f342696 --- /dev/null +++ b/os updates/kernel-reboot-notify/install-kernel-reboot-notify.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# install-kernel-reboot-notify.sh — idempotent. +# +# After a kernel update nothing tells the user to reboot. The new +# kernel + modules are on disk but the old kernel keeps running with its modules +# dir removed (can't load fresh modules), and the new UKI isn't booted. Installs: +# 1. nosignal-reboot-check -> ~/.local/bin (helper + desktop notify) +# 2. 95-nosignal-kernel-reboot.hook -> /etc/pacman.d/hooks (root; terminal warn) +# 3. a call to nosignal-reboot-check at the end of nosignal-update (idempotent) +# +# Re-runnable. The pacman-hook step needs root; if not root, it is skipped with a +# notice (run once as root, or let the builder ship the hook in the ISO). +set -eu + +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +BIN_DIR="$HOME/.local/bin" +mkdir -p "$BIN_DIR" + +# 1. helper +install -m 0755 "$HERE/nosignal-reboot-check" "$BIN_DIR/nosignal-reboot-check" +echo "installed $BIN_DIR/nosignal-reboot-check" + +# 2. pacman hook (root) +HOOK_DST=/etc/pacman.d/hooks/95-nosignal-kernel-reboot.hook +if [ "$(id -u)" -eq 0 ]; then + install -Dm 0644 "$HERE/95-nosignal-kernel-reboot.hook" "$HOOK_DST" + echo "installed $HOOK_DST" +elif command -v sudo >/dev/null 2>&1; then + sudo install -Dm 0644 "$HERE/95-nosignal-kernel-reboot.hook" "$HOOK_DST" \ + && echo "installed $HOOK_DST (via sudo)" \ + || echo "WARN: could not install pacman hook (need root) — skipped" +else + echo "WARN: not root and no sudo — pacman hook NOT installed ($HOOK_DST)" +fi + +# 3. wire into nosignal-update (idempotent; edits the real file behind the symlink) +NU=$(command -v nosignal-update 2>/dev/null || echo "$BIN_DIR/nosignal-update") +NU=$(readlink -f -- "$NU" 2>/dev/null || echo "$NU") +MARKER="# >>> nosignal kernel-reboot-notify >>>" +if [ -f "$NU" ] && ! grep -qF "$MARKER" "$NU"; then + # insert the call just before the final 'NoSignal up to date' log line + tmp=$(mktemp) + awk -v marker="$MARKER" ' + /log "NoSignal up to date"/ && !done { + print marker + print "command -v nosignal-reboot-check >/dev/null 2>&1 && nosignal-reboot-check || true" + print "# <<< nosignal kernel-reboot-notify <<<" + done=1 + } + { print } + ' "$NU" > "$tmp" && cat "$tmp" > "$NU" && rm -f "$tmp" + echo "wired nosignal-reboot-check into $NU" +else + echo "nosignal-update already wired (or not found at $NU) — skipped" +fi + +echo "kernel-reboot-notify: ok" diff --git a/os updates/kernel-reboot-notify/nosignal-reboot-check b/os updates/kernel-reboot-notify/nosignal-reboot-check new file mode 100755 index 0000000..c2e8f38 --- /dev/null +++ b/os updates/kernel-reboot-notify/nosignal-reboot-check @@ -0,0 +1,41 @@ +#!/bin/sh +# nosignal-reboot-check — tell the user to reboot when a kernel update has landed. +# +# Signal: a kernel package upgrade replaces /usr/lib/modules/. If the +# running kernel's modules directory is gone, the new kernel is on disk but not +# running — the live session can't load fresh modules and the new UKI isn't +# booted yet → a reboot is required. Writes a stamp so a login check can re-warn +# until the reboot actually happens; clears it once running == on-disk again. +# +# Safe to run anytime (read-only except the stamp). Used by nosignal-update at +# the end of an update, and may be called at login. +set -u + +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/nosignal" +STAMP="$STATE_DIR/reboot-required" +running=$(uname -r) + +if [ -d "/usr/lib/modules/$running" ]; then + # Running kernel still installed → nothing pending; clear any stale stamp. + [ -f "$STAMP" ] && rm -f "$STAMP" + exit 0 +fi + +# Running kernel's modules were removed by an update → reboot needed. +mkdir -p "$STATE_DIR"; : > "$STAMP" +newest=$(ls -1 /usr/lib/modules/ 2>/dev/null | grep -vx "$running" | sort -V | tail -1) +msg="Kernel updated — reboot to load it" +[ -n "$newest" ] && msg="$msg ($running → $newest)" +msg="$msg." + +# Terminal banner (visible when run from nosignal-update). +printf '\n\033[1;33m:: %s\033[0m\n' "$msg" +printf '\033[1;33m:: At the Limine menu, boot the UKI entry (\"Linux\"/NoSignal UKI).\033[0m\n\n' + +# Desktop notification (best effort; only meaningful inside a user session). +if command -v notify-send >/dev/null 2>&1; then + notify-send -u critical -i system-reboot "NoSignal: reboot required" \ + "$msg +Boot the UKI entry at the Limine menu." 2>/dev/null || true +fi +exit 0 diff --git a/os updates/launcher-fix/README.md b/os updates/launcher-fix/README.md new file mode 100755 index 0000000..c32d6e8 --- /dev/null +++ b/os updates/launcher-fix/README.md @@ -0,0 +1,55 @@ +# launcher-fix — Super+Space opens the launcher and keeps it open + +Bug fix for omarchy-keys (Omarchy keybindings). + +## Symptom + +`Super+Space` flashes the app launcher open, then it immediately disappears — +it won't stay. + +## Root cause + +caelestia runs all its binds in a custom Hyprland submap called `global` with a +tap-to-launch system (`modules/Shortcuts.qml`): + +``` +bindi = Super, Super_L, global, caelestia:launcher # tap Super = launcher +bindin = Super, catchall, global, caelestia:launcherInterrupt # any key + Super = interrupt +``` + +- The `launcher` shortcut toggles **on release**, only `if (!launcherInterrupted)`. +- `launcherInterrupt` sets `launcherInterrupted = true`. +- **`catchall` matches Space**, so the same Space press that fires omarchy-keys' + `Super, Space -> caelestia:launcher` also fires `launcherInterrupt` → the + launcher is dismissed in the same keystroke. + +## Fix + +Bind `Super+Space` to toggle the launcher drawer **directly** via IPC, which is +independent of the `launcherInterrupted` flag: + +``` +unbind = Super, Space +bind = Super, Space, exec, qs -c caelestia ipc call drawers toggle launcher +``` + +Native Super-tap still opens the launcher (unchanged). No package change; no +edits to the caelestia clone. + +## Files + +- binds appended to `~/.config/caelestia/hypr-user.conf` (idempotent marked block) + +## Install + +```sh +sh install-launcher-fix.sh +``` + +## Builder notes — fold into omarchy-keys + +In `omarchy-keys/omarchy-keys-user.conf`, replace +`bind = Super, Space, global, caelestia:launcher` with +`bind = Super, Space, exec, qs -c caelestia ipc call drawers toggle launcher`. +(Upstream-worthy: caelestia's `catchall` interrupt should exclude keys that are +themselves explicitly bound to open the launcher.) diff --git a/os updates/launcher-fix/install-launcher-fix.sh b/os updates/launcher-fix/install-launcher-fix.sh new file mode 100755 index 0000000..b8521ac --- /dev/null +++ b/os updates/launcher-fix/install-launcher-fix.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# install-launcher-fix.sh — make Super+Space reliably open and KEEP the app +# launcher open. Idempotent. +# +# Bug: omarchy-keys binds `Super, Space -> global, caelestia:launcher`. But caelestia +# runs a custom `global` submap where `bindin = Super, catchall -> +# launcherInterrupt` fires on ANY key pressed with Super — including Space. The +# native launcher toggles on RELEASE only `if (!launcherInterrupted)`, so the +# Space that opens it also trips the interrupt and it won't stay. +# +# Fix: open the launcher via the drawer-toggle IPC directly, which is +# independent of the launcherInterrupted state machine. +set -eu + +HYPRUSER="$HOME/.config/caelestia/hypr-user.conf" +MARK='launcher-fix: Super+Space via drawers IPC' + +if [ ! -f "$HYPRUSER" ]; then + echo "NOTE: $HYPRUSER not found — rebind Super+Space manually." +elif grep -qF "$MARK" "$HYPRUSER"; then + echo ":: Super+Space already fixed in $HYPRUSER" +else + cat >> "$HYPRUSER" <<'EOF' + +# >>> launcher-fix: Super+Space via drawers IPC >>> +# Toggle the launcher drawer directly (bypasses caelestia's launcherInterrupt, +# which Space trips via the `Super, catchall` interrupt bind). Native Super-tap +# still opens the launcher as before. +unbind = Super, Space +bind = Super, Space, exec, qs -c caelestia ipc call drawers toggle launcher # Launch apps +# <<< launcher-fix: Super+Space via drawers IPC <<< +EOF + echo ":: rebound Super+Space -> drawers toggle launcher" +fi + +if command -v hyprctl >/dev/null 2>&1 && hyprctl version >/dev/null 2>&1; then + hyprctl reload >/dev/null 2>&1 && echo ":: reloaded Hyprland" +fi + +echo "Done. Super+Space opens the launcher and it stays open." diff --git a/os updates/limine-uki-dead-entry/README.md b/os updates/limine-uki-dead-entry/README.md new file mode 100755 index 0000000..c4a9409 --- /dev/null +++ b/os updates/limine-uki-dead-entry/README.md @@ -0,0 +1,68 @@ +# limine-uki-dead-entry + +The repair script edits the bootloader, so run it with care. Workaround: pick the UKI entry. + +## Symptom +After a `nosignal-update` that bumped the kernel (7.0.11→7.0.12), selecting +**"NoSignal OS"** in Limine drops to a **TTY**. Selecting **"Linux"** (the UKI +entry) boots normally. + +## Root cause +- `/etc/default/limine` sets **`ENABLE_UKI=yes`**. +- Per `limine-entry-tool.conf:151`: *"Duplicate 'initramfs' and 'vmlinuz' files + are removed when 'limine-mkinitcpio' or 'limine-update' is run to generate a + UKI."* So the first kernel update **deletes** `/boot/vmlinuz-linux` and + `/boot/initramfs-linux.img`. +- But `limine.conf` carries an installer-seeded **first** entry: + ``` + /NoSignal (Arch Linux) + protocol: linux + path: boot():/vmlinuz-linux + module_path: boot():/initramfs-linux.img + … + ``` + pointing at the now-deleted files. With **no `default_entry`**, this dead + entry is the **auto-boot default** → TTY. +- The auto-generated UKI entry (`protocol: efi` → `/EFI/Linux/nosignal_linux.efi`) + is correct and bootable — that's the "Linux" entry that worked. + +### Evidence +- `vmlinuz-linux` / `initramfs-linux.img`: **absent**. +- Dead entry present in both `limine.conf` and the pre-update `limine.conf.old` + (shipped from the ISO). +- UKI is **current**: its sha256 equals the freshly-built image — not stale + (the May-28 mtime is merely preserved). So the UKI is not the problem. +- GPU-agnostic; prior AMD rounds likely never ran a post-install kernel update. + +### Secondary risk (NVIDIA-specific) — flag for the builder +The UKI is **139 MB** (NVIDIA driver + GSP firmware; `linux-firmware-nvidia` is +214 MB) on a **511 MB** ESP. limine.conf already shows the live UKI + a history +copy. As more kernel versions / snapshot UKIs accumulate, the ESP can fill, and +then UKI writes *would* genuinely fail (a real stale-UKI scenario). Recommend: +size the ESP ≥1–2 GB on NVIDIA, and/or trim the initramfs (don't embed the full +GSP firmware), and/or keep UKI snapshot history off the ESP. + +## Fix +**Durable (builder, do this in the ISO):** don't seed a `protocol: linux` entry +when `ENABLE_UKI=yes`. Either +1. seed the OS entry as `protocol: efi` → `/EFI/Linux/_linux.efi`, or +2. drop the manual entry and let limine-entry-tool's auto UKI entry be the OS + entry (name it via `TARGET_OS_NAME`), and +3. set an explicit **`default_entry`** to the UKI so auto-boot never lands on a + bad entry. + +**This component (repair an installed system):** `fix-limine-uki-entry.sh` +(root, idempotent, backs up limine.conf) rewrites the seeded entry's body from +`protocol: linux` + `vmlinuz-linux` to `protocol: efi` + the UKI, keeping the +label (stays first = default) and the cmdline. No-op if already fixed; refuses +if the UKI file is missing. + +## Verify +``` +grep -A3 'NoSignal (Arch Linux)' /boot/limine.conf # protocol: efi + UKI path +``` +Then reboot and confirm the top/default entry boots to the desktop. + +## Files +- `fix-limine-uki-entry.sh` — idempotent root repair. +- `migrations/1781434800-limine-uki-dead-entry.sh` — delegates to it. diff --git a/os updates/limine-uki-dead-entry/fix-limine-uki-entry.sh b/os updates/limine-uki-dead-entry/fix-limine-uki-entry.sh new file mode 100755 index 0000000..854f28b --- /dev/null +++ b/os updates/limine-uki-dead-entry/fix-limine-uki-entry.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# fix-limine-uki-entry.sh — idempotent. REQUIRES ROOT. +# +# With ENABLE_UKI=yes, limine removes /boot/vmlinuz-linux and +# /boot/initramfs-linux.img (UKI-only), but the installer-seeded first entry +# "/NoSignal (Arch Linux)" in limine.conf still uses `protocol: linux` -> +# boot():/vmlinuz-linux. That entry is the default auto-boot target and now +# dead-boots to a TTY. This converts it to a `protocol: efi` entry that boots +# the UKI, keeping the same label (so it stays first = default) and cmdline. +# +# Idempotent: if the entry is already protocol:efi (no vmlinuz path), it's a +# no-op. Always backs up limine.conf first. Refuses if the UKI is missing. +# +# NOTE: the durable fix belongs in the ISO installer's seeded limine.conf / +# limine config — see README. This repairs an already-installed system. +set -eu + +ESP_PATH=/boot +UKI_NAME=nosignal +[ -r /etc/default/limine ] && . /etc/default/limine 2>/dev/null || true +[ -n "${ESP_PATH:-}" ] || ESP_PATH=/boot +[ -n "${CUSTOM_UKI_NAME:-}" ] && UKI_NAME="$CUSTOM_UKI_NAME" + +CONF="$ESP_PATH/limine.conf" +UKI_REL="/EFI/Linux/${UKI_NAME}_linux.efi" +UKI_ABS="$ESP_PATH$UKI_REL" + +[ "$(id -u)" -eq 0 ] || { echo "must run as root (edits $CONF)" >&2; exit 1; } +[ -f "$CONF" ] || { echo "no $CONF" >&2; exit 1; } +[ -f "$UKI_ABS" ] || { echo "UKI not found at $UKI_ABS — aborting (won't point at a missing file)" >&2; exit 1; } + +# Already fixed? (the seeded entry no longer references vmlinuz) +if ! grep -q 'path: boot():/vmlinuz-linux' "$CONF"; then + echo "limine.conf has no dead vmlinuz entry — nothing to do" + exit 0 +fi + +cp -a "$CONF" "$CONF.bak.$(date +%Y%m%d%H%M%S)" + +# Rewrite ONLY the manual entry's body: protocol:linux + vmlinuz + module_path +# lines become protocol:efi + UKI path. Keeps the `cmdline:` line as-is. +awk -v uki="$UKI_REL" ' + /^\/NoSignal \(Arch Linux\)/ { print; inblk=1; next } + inblk && /^[^[:space:]]/ { inblk=0 } # next top-level entry ends the block + inblk && /^[[:space:]]*protocol:[[:space:]]*linux/ { print " protocol: efi"; next } + inblk && /^[[:space:]]*path:[[:space:]]*boot\(\):\/vmlinuz-linux/ { print " path: boot():" uki; next } + inblk && /^[[:space:]]*module_path:/ { next } # drop ucode/initramfs modules (UKI is self-contained) + { print } +' "$CONF" > "$CONF.tmp" + +# sanity: must still contain our UKI path and no vmlinuz path +grep -q "path: boot():$UKI_REL" "$CONF.tmp" && ! grep -q 'boot():/vmlinuz-linux' "$CONF.tmp" \ + || { echo "rewrite sanity check failed — leaving $CONF untouched" >&2; rm -f "$CONF.tmp"; exit 1; } + +mv "$CONF.tmp" "$CONF" +sync +echo "fixed: '/NoSignal (Arch Linux)' now protocol:efi -> $UKI_REL (backup kept)" diff --git a/os updates/menu-cleanup/README.md b/os updates/menu-cleanup/README.md new file mode 100755 index 0000000..f7fc781 --- /dev/null +++ b/os updates/menu-cleanup/README.md @@ -0,0 +1,36 @@ +# menu-cleanup — hide uuctl from the launcher + +## Symptom + +`uuctl` appears in the Super+Space launcher. It is uwsm's user-systemd- +unit manager — a dmenu-driven admin utility that ships a desktop entry +with the `uwsm` package. Not an app a user should be offered; the +change "system-polish" hide list (avahi/foot/qv4l2/pinentry…) simply +missed it because it was specified before uwsm's entry was noticed. + +## Fix + +Same mechanism as system-polish A1: a user-level +`~/.local/share/applications/uuctl.desktop` override with +`NoDisplay=true` + `Hidden=true` shadows the system entry. Idempotent, +no sudo. + +## Packaging + +Fold `uuctl` into the `HIDDEN` list in +`system-polish/install-system-polish.sh` (one word) — this component +then only matters as a migration for boxes installed from older ISOs. + +## Considered and left visible (builder judgment calls) + +- `vim.desktop` — console Vim alongside Neovim; harmless, some users + want it. +- `limine-snapper-restore.desktop` — GUI snapshot restore; arguably a + recovery feature worth keeping discoverable. +- `cups.desktop` ("Manage Printing") + `system-config-printer.desktop` + ("Print Settings") — mild duplication, both functional. + +## Test + +`Super+Space` → type "uuctl" → no result. (Entries for the items above +still present.) diff --git a/os updates/menu-cleanup/install-menu-cleanup.sh b/os updates/menu-cleanup/install-menu-cleanup.sh new file mode 100755 index 0000000..eddc22a --- /dev/null +++ b/os updates/menu-cleanup/install-menu-cleanup.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# install-menu-cleanup.sh — hide launcher entries that should not appear. +# Currently: uuctl (uwsm's user-unit +# manager — a dmenu utility, not an app; ships a desktop entry via the uwsm +# package). +# +# Same mechanism as system-polish A1: a user-level override shadows the +# system entry. Safe to re-run (idempotent). No sudo. +set -eu + +APPS="$HOME/.local/share/applications" +HIDDEN="uuctl" + +mkdir -p "$APPS" +for id in $HIDDEN; do + [ -f "/usr/share/applications/$id.desktop" ] || continue + [ -f "$APPS/$id.desktop" ] && continue + cat > "$APPS/$id.desktop" <