From 3fb4117619a467c7262c750035dcaee104a4cd5b Mon Sep 17 00:00:00 2001 From: NeilClack Date: Sat, 11 Apr 2026 15:27:05 -0500 Subject: [PATCH 01/22] Cleanup repository from un-used files and directories --- docs/greetd-autologin-setup.md | 67 ------------ docs/kiosk-setup-guide.md | 189 --------------------------------- scripts/setup.sh | 64 ----------- 3 files changed, 320 deletions(-) delete mode 100644 docs/greetd-autologin-setup.md delete mode 100644 docs/kiosk-setup-guide.md delete mode 100755 scripts/setup.sh diff --git a/docs/greetd-autologin-setup.md b/docs/greetd-autologin-setup.md deleted file mode 100644 index 4ee2d08..0000000 --- a/docs/greetd-autologin-setup.md +++ /dev/null @@ -1,67 +0,0 @@ -# Greetd Auto-Login Setup for Ubuntu Server 24 - -## Overview -This guide configures greetd for automatic login of the `kiosk` user on Ubuntu Server 24, bypassing password authentication while keeping the user account password-protected for other purposes. - -## Installation - -```bash -# Install greetd -sudo apt update -sudo apt install greetd - -# Disable other display managers if running -sudo systemctl disable gdm3 lightdm sddm -``` - -## Configuration - -Edit `/etc/greetd/config.toml`: - -```toml -[terminal] -vt = 1 - -[default_session] -command = "agreety --cmd /bin/bash" -user = "kiosk" - -[initial_session] -command = "/bin/bash" -user = "kiosk" -``` - -## Auto-Start Application - -Create `/home/kiosk/.bash_profile`: - -```bash -#!/bin/bash -# Auto-start RhemaWave on login -if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then - exec /usr/local/bin/rhemawave -fi -``` - -Make it executable: -```bash -chmod +x /home/kiosk/.bash_profile -chown kiosk:kiosk /home/kiosk/.bash_profile -``` - -## Enable Service - -```bash -sudo systemctl enable greetd -sudo systemctl start greetd -``` - -## Verification - -Reboot the system. The `kiosk` user should auto-login on tty1 and launch RhemaWave automatically. - -## Notes - -- The `kiosk` user password remains active for SSH, sudo, and manual logins on other TTYs -- Auto-login only applies to tty1 (the primary console) -- If RhemaWave crashes, the user remains logged in at a bash prompt diff --git a/docs/kiosk-setup-guide.md b/docs/kiosk-setup-guide.md deleted file mode 100644 index 9e1b49c..0000000 --- a/docs/kiosk-setup-guide.md +++ /dev/null @@ -1,189 +0,0 @@ -# RhemaWave Kiosk Setup Guide - Ubuntu Server 24 - -Complete setup guide for a fresh Ubuntu Server 24 installation configured as a RhemaWave kiosk. - -## Initial System Setup - -### 1. Create Kiosk User - -```bash -sudo adduser kiosk -sudo usermod -aG sudo kiosk -``` - -### 2. Install Dependencies - -```bash -sudo apt update -sudo apt install -y \ - libsdl2-2.0-0 \ - libsdl2-ttf-2.0-0 \ - libsdl2-mixer-2.0-0 \ - libegl1-mesa \ - libgles2-mesa \ - libgbm1 \ - libinput10 \ - libxkbcommon0 \ - dbus \ - network-manager -``` - -### 3. Install Cage (Wayland Compositor) - -```bash -sudo apt install -y cage -``` - -### 4. Install Greetd - -```bash -sudo apt install -y greetd -``` - -### 5. Configure PAM for Greetd - -Create `/etc/pam.d/greetd`: - -```bash -sudo tee /etc/pam.d/greetd < -``` - -### View RhemaWave Logs -```bash -# From SSH session -sudo journalctl -u greetd -f -``` - -### Restart RhemaWave -```bash -sudo systemctl restart greetd -``` - -### Update RhemaWave -```bash -sudo apt update -sudo apt upgrade rhemawave -``` diff --git a/scripts/setup.sh b/scripts/setup.sh deleted file mode 100755 index d2ec981..0000000 --- a/scripts/setup.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash -set -e - -# RhemaWave Repository Setup Script -# Usage: curl -fsSL https://repository.rhemawavetech.com/setup.sh | sudo bash - -echo "=== RhemaWave Repository Setup ===" - -# Check if running as root -if [ "$EUID" -ne 0 ]; then - echo "Error: This script must be run as root" - echo "Please run: curl -fsSL https://repository.rhemawavetech.com/setup.sh | sudo bash" - exit 1 -fi - -# Check if running on Ubuntu/Debian -if [ ! -f /etc/debian_version ]; then - echo "Error: This script is for Ubuntu/Debian systems only" - exit 1 -fi - -REPO_URL="https://repository.rhemawavetech.com" -REPO_NAME="rhemawave" -SOURCES_LIST="/etc/apt/sources.list.d/${REPO_NAME}.list" -KEYRING_FILE="/usr/share/keyrings/${REPO_NAME}-archive-keyring.gpg" - -echo "Adding RhemaWave repository..." - -# Add the repository to sources.list.d -# echo "deb [signed-by=${KEYRING_FILE}] ${REPO_URL} stable main" >"${SOURCES_LIST}" - -echo "deb [trusted=yes] ${REPO_URL} release main" >"${SOURCES_LIST}" - -# Download and install the GPG key (you'll need to provide this) -# Uncomment and modify once you have a GPG key: -# curl -fsSL "${REPO_URL}/KEY.gpg" | gpg --dearmor -o "${KEYRING_FILE}" - -echo "Updating package lists..." -apt update - -echo "Upgrading system packages..." -apt upgrade -y - -echo "Installing Tailscale..." -if ! command -v tailscale &> /dev/null; then - curl -fsSL https://tailscale.com/install.sh | sh -else - echo "✓ Tailscale already installed" -fi - -echo "Installing RhemaWave..." -apt install -y rhemawave - -echo "Setting Tailscale operator..." -if tailscale set --operator=kiosk; then - echo "✓ Tailscale operator set to kiosk" -else - echo "⚠ Warning: Could not set Tailscale operator" -fi - -echo "" -echo "=== Installation Complete ===" -echo "RhemaWave has been installed successfully." -echo "" \ No newline at end of file From 337710c8e73b39f6062c6eeaf3efdcc7d6b48c1d Mon Sep 17 00:00:00 2001 From: NeilClack Date: Mon, 13 Apr 2026 07:44:48 -0500 Subject: [PATCH 02/22] Update tasks --- tasks.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tasks.md diff --git a/tasks.md b/tasks.md new file mode 100644 index 0000000..57302a4 --- /dev/null +++ b/tasks.md @@ -0,0 +1,24 @@ +- [\-] Audio device does not always initialize when starting scroller. +- [ ] Each proclamation should play for between 3 and 5 minutes before continuing to the next. +- [ ] Proclamations should be downloaded again when the Proclamation Options page is opened. +- [ ] Proclamation downloads should be a sync, not a complete overwrite, and should handle cases +where items missing from the database are removed from the application. +- [ ] Real license keys still need to be implemented. The functionality exists to check them, but they are not checked. +- [ ] Migrate from SDL2 to SDL3. +- [ ] Migrate from OpenGL Core to OpenGL GLES for compatibility on ARM devices. +- [ ] Prevent switching screens during "pre-run" checks such as WiFi, Email, and License Key. +- [ ] TOS pages need to be re-added. +- [ ] Create a short tutorial of how to use the application. +- [ ] Investigate playing video from the application, try using SDL. +- [ ] WiFi screen has a bug causing users to believe they entered the wrong WiFi password when they have in-fact +connected to their WiFi successfully. +- [ ] Rotate the screen 90 degrees on demand. +- [ ] Force audio to always use HDMI connection. +- [ ] Create subscription tiers and block proclamations from being selected. +- [ ] Create schedules to play specific playlists of proclamations. +- [ ] Download Mr. Gouldd's music choices. +- [ ] Use the fileserver to obtain audio upon installation, not packaging it all in the binary. +- [ ] Fix the Attic server. +- [ ] Update deployed kiosks to use the new attic server. This will cause a rebuild on the machines +but I think that is okay. Rebuild issues were during ISO development, not kiosk development. +- [ ] All pages should share a font and font manager. Font Manager should be loaded prior to creating any pages. From 957583339262f226a87d45cdf0e37a4d23f6e233 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Mon, 13 Apr 2026 08:19:32 -0500 Subject: [PATCH 03/22] Cleanup repository --- MANUAL.md | 21 -- dev_scripts/reset.sh | 5 - maintainer-guide.md | 796 ------------------------------------------- 3 files changed, 822 deletions(-) delete mode 100644 MANUAL.md delete mode 100755 dev_scripts/reset.sh delete mode 100644 maintainer-guide.md diff --git a/MANUAL.md b/MANUAL.md deleted file mode 100644 index 3f5a586..0000000 --- a/MANUAL.md +++ /dev/null @@ -1,21 +0,0 @@ -# Rhema Wave User Manual - -## Keybinds -These are the basic keybinds for navigating Rhema Wave and operating the program. -Should you find a keybind that does something and is not listed here, please email nclack@rhemawavetech.com - -> When you see key combinations such as "SHIFT + UP ARROW", press and hold the first key, in this case SHIFT -> then press the second key. Do not hold the second key as right now, there is no way to stop a flood of commands -> from being sent to the program, and you could send more than the program can handle, causing things to "continue happening after you've let go of the keys". - - -### Navigation - -**F1** - The "Welcome" screen. The screen that appears when you first start Rhema Wave and is useful for finding the current version of Rhema Wave you have installed. -**F2** - The "Player". This is the main application where your proclamations and audio will be played. -**F3** - The "Settings" menu. Use this menu to connect to WiFi, manage Audio Output Devices, and select your active proclamations to be played. - -### Controls - -**SHIFT + UP ARROW** - Volume Up -**SHIFT + DOWN ARROW** - Volume Down diff --git a/dev_scripts/reset.sh b/dev_scripts/reset.sh deleted file mode 100755 index 17c420a..0000000 --- a/dev_scripts/reset.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -rm -rf ~/.local/share/rhemawave/ - -echo "Data directory deleted, application reset." diff --git a/maintainer-guide.md b/maintainer-guide.md deleted file mode 100644 index 42685a1..0000000 --- a/maintainer-guide.md +++ /dev/null @@ -1,796 +0,0 @@ -# RhemaWave Kiosk Infrastructure — Complete Maintainer Guide - -## Purpose - -This document describes the **complete** infrastructure for building, distributing, and deploying the RhemaWave kiosk application on NixOS. It is written so that a new developer can reconstruct the entire system from scratch on a fresh machine. - ---- - -## Table of Contents - -1. Architecture Overview -2. Repositories -3. Key Files and Secrets — What Lives Where -4. Development Machine Setup (From Scratch) -5. The RhemaWave Flake (Application) -6. The NixConfig Flake (Kiosk System) -7. Binary Cache Setup -8. Caddy Server Configuration -9. Signing Keys -10. Sops-Nix Secrets Management -11. GitHub Access -12. Building and Publishing a New Release -13. Deploying a New Kiosk (nixos-anywhere) -14. Auto-Update System -15. Testing and Debugging -16. Common Errors and Solutions - ---- - -## 1. Architecture Overview - -``` -┌─────────────────────────────────┐ -│ Developer Machine (P15) │ -│ │ -│ RhemaWave repo NixConfig repo│ -│ (Go application) (NixOS cfg) │ -│ │ │ │ -│ nix build nix flake │ -│ nix store sign update │ -│ nix copy git push │ -│ │ │ │ -└─────────┼───────────────┼───────┘ - │ │ - ▼ ▼ -┌─────────────────┐ ┌──────────┐ -│ Web Server │ │ GitHub │ -│ (Caddy) │ │ (private)│ -│ │ │ │ -│ Binary cache: │ │ NixConfig│ -│ repository. │ │ flake │ -│ rhemawavetech │ │ │ -│ .com │ │ RhemaWave│ -│ │ │ flake │ -│ Static files: │ │ │ -│ .narinfo + .nar │ └────┬─────┘ -└────────┬────────┘ │ - │ │ - ▼ ▼ -┌─────────────────────────────────┐ -│ Kiosk Machines │ -│ │ -│ systemd timer (every 15min): │ -│ 1. Fetch NixConfig from │ -│ GitHub │ -│ 2. Evaluate flake │ -│ 3. Download RhemaWave binary │ -│ from binary cache │ -│ 4. nixos-rebuild switch │ -│ │ -│ Cage compositor → RhemaWave │ -│ (fullscreen, locked, no TTY │ -│ switching, auto-restart) │ -└─────────────────────────────────┘ -``` - ---- - -## 2. Repositories - -| Repo | URL | Branch | Contents | -|------|-----|--------|----------| -| RhemaWave | `github:Rhema-Wave-Technologies/RhemaWave` | `dev` | Go application source, `flake.nix` (package + NixOS module), `nixos/module.nix` | -| NixConfig | `github:Rhema-Wave-Technologies/NixConfig` | `main` | NixOS kiosk configuration, disk layout, encrypted secrets, sops config | - -Both repositories are **private**. Access requires a GitHub fine-grained personal access token with read-only Contents permission on both repos. - ---- - -## 3. Key Files and Secrets — What Lives Where - -### On the Developer Machine - -| File | Path | Purpose | Backup? | -|------|------|---------|---------| -| Binary cache private key | `~/Keys/nix/cache/rhemawave/cache-priv-key.pem` | Signs packages for the binary cache | **CRITICAL** — without this, you cannot publish new builds. Back up to encrypted offline storage. | -| Binary cache public key | `~/Keys/nix/cache/rhemawave/cache-pub-key.pem` | Distributed to all client machines for verification | Can be regenerated from private key | -| Sops age private key | `~/Keys/nix/sops/age-key.txt` | Encrypts/decrypts secrets in NixConfig | **CRITICAL** — without this, you cannot edit secrets. Back up to encrypted offline storage. | -| GitHub access token | `~/.config/nix/nix.conf` | Nix uses this to access private GitHub repos | Can be regenerated from GitHub | -| Sops age key env var | `~/.bashrc` | `SOPS_AGE_KEY_FILE=~/Keys/nix/sops/age-key.txt` | Convention only | - -### On the Web Server - -| File | Path | Purpose | -|------|------|---------| -| Binary cache files | `/var/lib/nix-cache/` | Static files served by Caddy | -| `nix-cache-info` | `/var/lib/nix-cache/nix-cache-info` | Metadata file for Nix clients | -| Caddy config | Caddyfile (location varies) | Serves the cache directory | - -The server does **not** need any signing keys. It is a dumb static file host. - -### On Each Kiosk - -| File | Path | Purpose | -|------|------|---------| -| SSH host key (private) | `/etc/ssh/ssh_host_ed25519_key` | Used by sops-nix to decrypt secrets | -| SSH host key (public) | `/etc/ssh/ssh_host_ed25519_key.pub` | Converted to age key for sops `.sops.yaml` | -| Decrypted secrets | `/run/secrets/*` | Runtime-only, never persisted to disk | - -### What to Back Up (Priority Order) - -1. **`~/Keys/nix/cache/rhemawave/cache-priv-key.pem`** — If lost, you must generate a new key pair and update the public key in every kiosk configuration and redeploy. -2. **`~/Keys/nix/sops/age-key.txt`** — If lost, you must re-encrypt all secrets with a new key. -3. **GitHub token** — Easily regenerated, low priority. - -Store backups on an encrypted USB drive or a password manager's secure notes. Never store these in any Git repository, cloud drive, or unencrypted medium. - ---- - -## 4. Development Machine Setup (From Scratch) - -Starting from a fresh NixOS installation on a development machine. - -### 4.1 NixOS Configuration Prerequisites - -In your system `configuration.nix`: - -```nix -nix.settings = { - experimental-features = [ "nix-command" "flakes" ]; - trusted-users = [ "root" "yourusername" ]; - substituters = [ - "https://cache.nixos.org" - "https://repository.rhemawavetech.com" - ]; - trusted-public-keys = [ - "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" - "repository.rhemawavetech.com-1:hbaEpYz5jE9SeNgkF2jAgjMtjXzruj7lEk6Lvx9ll40=" - ]; -}; -nixpkgs.config.allowUnfree = true; -``` - -Rebuild: `sudo nixos-rebuild switch` - -### 4.2 Add the RhemaWave Flake to Your System Config - -In your system `flake.nix`, add the input and package: - -```nix -inputs.rhemawave = { - url = "github:Rhema-Wave-Technologies/RhemaWave/dev"; - inputs.nixpkgs.follows = "nixpkgs"; -}; -``` - -Add `rhemawave` to the `outputs` function arguments, then add to a module: - -```nix -{ - environment.systemPackages = [ - rhemawave.packages.x86_64-linux.default - ]; -} -``` - -### 4.3 GitHub Access Token - -Generate a fine-grained personal access token at `github.com/settings/tokens`: -- Resource owner: `Rhema-Wave-Technologies` -- Repository access: Select `NixConfig` and `RhemaWave` -- Permissions: Contents → Read-only -- Expiration: Set as appropriate - -Configure Nix to use it: - -```bash -mkdir -p ~/.config/nix -echo "access-tokens = github.com=YOUR_TOKEN_HERE" > ~/.config/nix/nix.conf -``` - -Also copy to root (needed for `sudo` operations): - -```bash -sudo mkdir -p /root/.config/nix -sudo cp ~/.config/nix/nix.conf /root/.config/nix/nix.conf -``` - -### 4.4 Signing Keys - -If you're taking over from an existing maintainer, obtain: -- `cache-priv-key.pem` → save to `~/Keys/nix/cache/rhemawave/cache-priv-key.pem` -- `age-key.txt` → save to `~/Keys/nix/sops/age-key.txt` - -If starting fresh (new cache): - -```bash -mkdir -p ~/Keys/nix/cache/rhemawave -nix key generate-secret --key-name repository.rhemawavetech.com-1 > ~/Keys/nix/cache/rhemawave/cache-priv-key.pem -nix key convert-secret-to-public < ~/Keys/nix/cache/rhemawave/cache-priv-key.pem > ~/Keys/nix/cache/rhemawave/cache-pub-key.pem -``` - -If starting fresh (new sops key): - -```bash -mkdir -p ~/Keys/nix/sops -nix shell nixpkgs#age -c age-keygen -o ~/Keys/nix/sops/age-key.txt -``` - -Note the public key (starts with `age1...`) — it goes in `.sops.yaml`. - -### 4.5 Sops Environment Variable - -Add to `~/.bashrc`: - -```bash -echo 'export SOPS_AGE_KEY_FILE=~/Keys/nix/sops/age-key.txt' >> ~/.bashrc -source ~/.bashrc -``` - -### 4.6 Clone the Repositories - -```bash -mkdir -p ~/Work/RhemaWave -cd ~/Work/RhemaWave -git clone git@github.com:Rhema-Wave-Technologies/RhemaWave.git -git clone git@github.com:Rhema-Wave-Technologies/NixConfig.git -``` - -### 4.7 Verify Everything Works - -```bash -# Test GitHub access -cd ~/Work/RhemaWave/NixConfig -nix flake show github:Rhema-Wave-Technologies/NixConfig - -# Test RhemaWave build -cd ~/Work/RhemaWave/RhemaWave -nix build .#default - -# Test binary runs -./result/bin/rhemawave - -# Test secrets decryption -cd ~/Work/RhemaWave/NixConfig -SOPS_AGE_KEY_FILE=~/Keys/nix/sops/age-key.txt nix shell nixpkgs#sops -c sops secrets.yaml -# Should open in editor with plaintext values -# Close without saving -``` - ---- - -## 5. The RhemaWave Flake (Application) - -Located at `~/Work/RhemaWave/RhemaWave/flake.nix`. - -### Key Points - -- Uses `buildGoModule` with `vendorHash = null` (vendor directory committed to repo) -- CGo is enabled (`env.CGO_ENABLED = "1"`) -- SDL2 mixer/ttf/image flags are set in `preBuild` via `pkg-config` -- The `share/` directory is copied to `$out/share` in `postInstall` -- License is set to `unfree` — consumers must set `allowUnfreePredicate` -- Exports `nixosModules.default` (the kiosk module) alongside per-system packages and dev shells - -### Flake Outputs - -| Output | Description | -|--------|-------------| -| `packages..default` | The RhemaWave binary | -| `devShells..default` | Development shell with Go + SDL2 deps | -| `nixosModules.default` | NixOS module for kiosk configuration (greetd, cage, PipeWire, polkit) | - -### The NixOS Module (`nixos/module.nix`) - -Imported via `import ./nixos/module.nix self` — the `self` argument gives the module access to the flake's packages. The module exposes `services.rhemawave.*` options. - ---- - -## 6. The NixConfig Flake (Kiosk System) - -Located at `~/Work/RhemaWave/NixConfig/flake.nix`. - -### Flake Inputs - -```nix -nixpkgs → github:NixOS/nixpkgs/nixpkgs-unstable -disko → github:nix-community/disko -nixos-facter → github:numtide/nixos-facter-modules -rhemawave → github:Rhema-Wave-Technologies/RhemaWave/dev -sops-nix → github:Mic92/sops-nix -``` - -All inputs follow `nixpkgs` to ensure a single nixpkgs version. - -### System Configuration Modules - -The NixOS system (`rhemawavekiosk`) loads these modules: -1. `disko.nixosModules.disko` — disk partitioning -2. `rhemawave.nixosModules.default` — RhemaWave kiosk module -3. `sops-nix.nixosModules.sops` — secrets management -4. `./configuration.nix` — main system config -5. Hardware facter report -6. Inline module adding `rhemawave.packages.x86_64-linux.default` to `environment.systemPackages` - ---- - -## 7. Binary Cache Setup - -### What It Is - -A Nix binary cache is a static HTTP file server with a specific directory structure. When a NixOS machine needs a package, it checks each configured cache by making HTTP GET requests for `.narinfo` files. If found and the signature is valid, it downloads the pre-built `.nar.xz` archive instead of building from source. - -### Directory Structure - -``` -/var/lib/nix-cache/ -├── nix-cache-info # Cache metadata -├── fv5iinvsldz9l2s8ikm0mw8ykhriad1g.narinfo # Package metadata + signature -├── nar/ -│ └── 1j7d9cif4xjq7i5rw5dmivxyninh7z1cm7zvdknsp8vjd9rjmzkc.nar.xz # Compressed package -└── ... (more .narinfo and .nar.xz files) -``` - -### nix-cache-info - -``` -StoreDir: /nix/store -WantMassQuery: 1 -Priority: 40 -``` - -This file must exist at the root. `Priority: 40` means this cache is checked after the official NixOS cache (priority 30). - -### Creating the Cache (One-Time) - -If setting up a new cache server: - -```bash -# On the server -sudo mkdir -p /var/lib/nix-cache -cat > /var/lib/nix-cache/nix-cache-info < cache-priv-key.pem -nix key convert-secret-to-public < cache-priv-key.pem > cache-pub-key.pem -``` - -### Deriving Public Key from Private Key - -```bash -nix key convert-secret-to-public < ~/Keys/nix/cache/rhemawave/cache-priv-key.pem -``` - -This outputs the public key string. It should match what's in every client's `trusted-public-keys`. - -### Current Public Key - -``` -repository.rhemawavetech.com-1:hbaEpYz5jE9SeNgkF2jAgjMtjXzruj7lEk6Lvx9ll40= -``` - ---- - -## 10. Sops-Nix Secrets Management - -### How It Works - -1. Secrets are encrypted in `secrets.yaml` using age public keys -2. `.sops.yaml` defines which keys can encrypt/decrypt -3. Each kiosk's SSH host key is converted to an age key for decryption -4. On activation, sops-nix decrypts secrets and places them at `/run/secrets/` - -### .sops.yaml - -```yaml -keys: - - &admin age1ptpn9zee8x7vjaaktx9dv65vysdjfmmgtj96wc3l49vd8g8huy6syk8l26 - - &kiosk-dev age1nmn0r66hl24wrz73q5htt0r90vfse9dvyjwae2evqwrealfgycfsy3apmy -creation_rules: - - path_regex: secrets\.yaml$ - key_groups: - - age: - - *admin - - *kiosk-dev -``` - -### Adding a New Kiosk - -1. Get the kiosk's SSH host public key: - ```bash - ssh root@KIOSK_IP "cat /etc/ssh/ssh_host_ed25519_key.pub" - ``` - -2. Convert to age key: - ```bash - nix shell nixpkgs#ssh-to-age -c ssh-to-age < <(ssh root@KIOSK_IP "cat /etc/ssh/ssh_host_ed25519_key.pub") - ``` - -3. Add the age key to `.sops.yaml` under `keys:` with a descriptive anchor (e.g., `&kiosk-prod-001`) - -4. Add the anchor reference to the creation rule's age list - -5. Re-encrypt secrets with the new key: - ```bash - SOPS_AGE_KEY_FILE=~/Keys/nix/sops/age-key.txt nix shell nixpkgs#sops -c sops updatekeys secrets.yaml - ``` - -6. Commit and push `.sops.yaml` and `secrets.yaml` - -### Editing Secrets - -```bash -cd ~/Work/RhemaWave/NixConfig -SOPS_AGE_KEY_FILE=~/Keys/nix/sops/age-key.txt nix shell nixpkgs#sops -c sops secrets.yaml -``` - -The file opens in your editor with plaintext values. Edit, save, close — sops re-encrypts automatically. - -### Secret Format for github-token - -The `github-token` secret must contain the full nix.conf line (because it's included via `!include`): - -```yaml -github-token: "access-tokens = github.com=ghp_yourtokenhere" -``` - ---- - -## 11. GitHub Access - -### Developer Machine - -The token is stored in `~/.config/nix/nix.conf`: - -``` -access-tokens = github.com=YOUR_TOKEN_HERE -``` - -Also needs to be in `/root/.config/nix/nix.conf` for `sudo` operations. - -Flake URLs use the `github:` scheme (not `git+https://`), which reads the `access-tokens` setting. - -### Kiosk Machines - -The token is managed via sops-nix and included in `nix.conf` via `!include /run/secrets/github-token`. - -### Token Scope - -Fine-grained personal access token: -- Resource owner: `Rhema-Wave-Technologies` -- Repositories: `NixConfig`, `RhemaWave` -- Permissions: Contents → Read-only - ---- - -## 12. Building and Publishing a New Release - -Complete procedure, every command, in order. All commands run on your development machine. - -### Step 1: Commit Application Changes - -```bash -cd ~/Work/RhemaWave/RhemaWave - -# If go.mod/go.sum changed: -nix shell nixpkgs#go -c go mod vendor -git add vendor/ - -git add -A -git commit -m "your changes" -git push origin dev -``` - -### Step 2: Build - -```bash -nix build .#default -``` - -### Step 3: Sign - -```bash -nix store sign --key-file ~/Keys/nix/cache/rhemawave/cache-priv-key.pem $(nix path-info ./result) -``` - -### Step 4: Export to Cache Format - -```bash -rm -rf /tmp/nix-cache -nix copy --to file:///tmp/nix-cache ./result --secret-key-files ~/Keys/nix/cache/rhemawave/cache-priv-key.pem -``` - -### Step 5: Verify Signature - -```bash -cat /tmp/nix-cache/*.narinfo | grep Sig -``` - -Must show `Sig: repository.rhemawavetech.com-1:...` - -### Step 6: Upload to Server - -```bash -rsync -av /tmp/nix-cache/ YOUR_USER@your-server:/var/lib/nix-cache/ -``` - -### Step 7: Bump NixConfig - -```bash -cd ~/Work/RhemaWave/NixConfig -nix flake update rhemawave -git add flake.lock -git commit -m "bump rhemawave" -git push -``` - -### Step 8: Wait - -Kiosks pick up the update within 15 minutes via the auto-update timer. - ---- - -## 13. Deploying a New Kiosk (nixos-anywhere) - -### Prerequisites - -- Target machine booted from NixOS installer USB -- SSH access to the target as root -- Target's IP address known - -### Procedure - -From your development machine: - -```bash -cd ~/Work/RhemaWave/NixConfig - -# Deploy -nix run github:nix-community/nixos-anywhere -- --flake .#rhemawavekiosk root@TARGET_IP -``` - -This: -1. kexec's into the NixOS installer on the target -2. Partitions the disk according to `disk-config.nix` -3. Installs NixOS with the full kiosk configuration -4. Reboots - -**Note:** Uses the local flake (`.#`) so it can access local files and secrets. The `--flake` path must be the NixConfig directory. - -### After Deployment - -1. Get the kiosk's age key (see section 10, "Adding a New Kiosk") -2. Add it to `.sops.yaml` and re-encrypt secrets -3. Push changes -4. The kiosk will pick up the updated secrets on its next auto-update cycle - -### If nixos-rebuild --target-host Fails with Bootloader Errors - -Use the manual three-step deploy instead: - -```bash -nix build .#nixosConfigurations.rhemawavekiosk.config.system.build.toplevel -nix copy --to ssh://root@TARGET_IP ./result -ssh root@TARGET_IP "$(readlink -f ./result)/bin/switch-to-configuration switch" -``` - ---- - -## 14. Auto-Update System - -### How It Works - -Each kiosk runs a systemd timer (`kiosk-auto-update.timer`) that fires every 15 minutes. The associated service runs: - -```bash -nixos-rebuild switch --flake github:Rhema-Wave-Technologies/NixConfig#rhemawavekiosk -``` - -This: -1. Fetches the latest NixConfig flake from GitHub (using the encrypted GitHub token) -2. Evaluates the configuration (including the pinned RhemaWave version from `flake.lock`) -3. Checks the binary cache for pre-built packages -4. Downloads any new packages -5. Switches to the new configuration - -### What Triggers an Update - -An update is triggered when either: -- `flake.lock` changes (new RhemaWave version pinned) -- `configuration.nix` or other NixConfig files change -- Any flake input is updated - -If nothing has changed, `nixos-rebuild` is a no-op. - -### Monitoring - -```bash -# Check timer status -ssh root@KIOSK_IP "systemctl status kiosk-auto-update.timer" - -# Check last update result -ssh root@KIOSK_IP "systemctl status kiosk-auto-update.service" - -# Full update logs -ssh root@KIOSK_IP "journalctl -u kiosk-auto-update -n 100" -``` - -### Force Immediate Update - -```bash -ssh root@KIOSK_IP "systemctl start kiosk-auto-update" -``` - ---- - -## 15. Testing and Debugging - -### SSH into a Running Kiosk - -Even with the kiosk locked down (cage compositor, single TTY), SSH always works: - -```bash -ssh root@KIOSK_IP -``` - -### Check What's Running - -```bash -ssh root@KIOSK_IP "systemctl status greetd" -ssh root@KIOSK_IP "systemctl status kiosk-auto-update.timer" -ssh root@KIOSK_IP "ls -la /run/secrets/" -ssh root@KIOSK_IP "cat /run/secrets/wifi-psk" # verify decryption -``` - -### Check Binary Cache Connectivity - -```bash -ssh root@KIOSK_IP "curl -s https://repository.rhemawavetech.com/nix-cache-info" -``` - -### Check GitHub Connectivity - -```bash -ssh root@KIOSK_IP "cat /etc/nix/nix.conf | grep access-tokens" -``` - -### Restart the Kiosk Application - -If the application crashes or you want to restart it without a full reboot: - -```bash -ssh root@KIOSK_IP "systemctl restart greetd" -``` - -greetd will restart, auto-login the kiosk user, and re-launch rhemawave in cage. - -### View Application Logs - -```bash -ssh root@KIOSK_IP "journalctl -u greetd -n 100" -``` - -### Test a Configuration Change Without Pushing - -Build and deploy directly from your development machine: - -```bash -cd ~/Work/RhemaWave/NixConfig -nix build .#nixosConfigurations.rhemawavekiosk.config.system.build.toplevel -nix copy --to ssh://root@KIOSK_IP ./result -ssh root@KIOSK_IP "$(readlink -f ./result)/bin/switch-to-configuration switch" -``` - ---- - -## 16. Common Errors and Solutions - -### `env attribute set cannot contain any attributes passed to derivation` - -**Cause:** `CGO_ENABLED` set as both a top-level attribute and in `env`. -**Fix:** Use only `env.CGO_ENABLED = "1";` in the `buildGoModule` block. - -### `vendor/modules.txt` inconsistency - -**Cause:** `go.mod` changed but `vendor/` wasn't updated. -**Fix:** Run `nix shell nixpkgs#go -c go mod vendor`, then `git add vendor/` and commit. - -### `unfree license, refusing to evaluate` - -**Cause:** Missing `allowUnfreePredicate` in the nixpkgs config. -**Fix:** Add `config.allowUnfreePredicate = pkg: (nixpkgs.lib.getName pkg) == "rhemawave";` to the `pkgs` import. - -### `warning: ignoring the client-specified setting 'secret-key-files'` - -**Cause:** Your user isn't in `trusted-users`. -**Fix:** Add your username to `nix.settings.trusted-users` and rebuild your NixOS system. - -### `invalid character in Base64 string` - -**Cause:** The `trusted-public-keys` value is malformed — likely doubled key name or invisible characters. -**Fix:** Regenerate the public key: `nix key convert-secret-to-public < cache-priv-key.pem` and paste it fresh into the config. - -### `attribute 'nixosModules' missing` - -**Cause:** The `flake.lock` points to an old RhemaWave commit that doesn't export `nixosModules`. -**Fix:** Run `nix flake update rhemawave` in the NixConfig directory. - -### `path '/home/...' is forbidden in pure evaluation mode` - -**Cause:** Using absolute paths (like `builtins.readFile /home/...`) in a flake. Flakes evaluate in pure mode. -**Fix:** Use relative paths (`./file.txt`) or manage secrets with sops-nix. - -### `access to absolute path '/home' is forbidden` - -**Cause:** Same as above — `readFile` with an absolute path. -**Fix:** Use sops-nix for secrets or deploy with the local flake (`.#`) instead of `github:`. - -### Bootloader errors during remote `nixos-rebuild --target-host` - -**Cause:** GRUB can't detect the disk layout properly over SSH. -**Fix:** Use the three-step manual deploy: `nix build` → `nix copy --to ssh://` → `switch-to-configuration switch`. - -### SSH host key changed warning - -**Cause:** After reinstalling a kiosk, its SSH host key changes. -**Fix:** `ssh-keygen -R KIOSK_IP` to remove the old key, then reconnect. - -### Auto-update not working on kiosk - -**Check:** -1. Timer is active: `systemctl status kiosk-auto-update.timer` -2. Service logs: `journalctl -u kiosk-auto-update -n 50` -3. GitHub token is decrypted: `cat /etc/nix/nix.conf | grep access-tokens` -4. Network connectivity: `curl https://github.com` -5. Cache connectivity: `curl https://repository.rhemawavetech.com/nix-cache-info` From 05a70988861044ab9c1fd9da5a3b6c849a529b05 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Mon, 13 Apr 2026 13:00:27 -0500 Subject: [PATCH 04/22] Remove devices list and device vector length check --- internal/mixer/mixer.go | 6 +++ internal/page/audioDevices.go | 76 ++++++++++++++----------------- internal/page/emailAddressPage.go | 8 ++-- internal/page/home.go | 12 ++--- internal/page/licenseKeyPage.go | 16 +++---- internal/page/logViewerPage.go | 6 +-- internal/page/message.go | 2 +- internal/page/scrollerPage.go | 25 ++++------ 8 files changed, 71 insertions(+), 80 deletions(-) diff --git a/internal/mixer/mixer.go b/internal/mixer/mixer.go index 54880cc..65da5d4 100644 --- a/internal/mixer/mixer.go +++ b/internal/mixer/mixer.go @@ -103,13 +103,17 @@ func openDeviceLocked(name string) error { // Play loads a WAV (as music) and plays it. If loopForever is true, loops forever. // If something is already playing, it stops and replaces it. func Play(path string, loopForever bool, cfg *db.AppConfig) error { + // Check if audio is enabled or disabled if !cfg.AudioEnabled { return nil } + // Lock the mutex mu.Lock() defer mu.Unlock() + // Check if mixer has been initialized and that a device is open + // JUMP: This is where the issue is... if !inited || !deviceOpen { logger.Error("audio mixer not initialized") return errors.New("audio mixer not initialized") @@ -126,10 +130,12 @@ func Play(path string, loopForever bool, cfg *db.AppConfig) error { currentMusic = nil } + // Load the music file m, err := mix.LoadMUS(path) if err != nil { return fmt.Errorf("LoadMUS(%q): %w", path, err) } + // Set current music to the loaded music file currentMusic = m currentPath = path if loopForever { diff --git a/internal/page/audioDevices.go b/internal/page/audioDevices.go index 1bb7c3b..b4fd897 100644 --- a/internal/page/audioDevices.go +++ b/internal/page/audioDevices.go @@ -5,7 +5,6 @@ import ( "github.com/go-gl/gl/v3.3-core/gl" "github.com/veandco/go-sdl2/sdl" - "gorm.io/gorm" "rhemawavetech.com/rhemawave/internal/colors" "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" @@ -25,7 +24,7 @@ type AudioDevices struct { selectedIndex int verticalMargin int32 CfgPtr *db.AppConfig - dbPtr *gorm.DB + // DbPtr *gorm.DB } func (p *AudioDevices) Init(ctx *PageCtx) error { @@ -57,7 +56,7 @@ func (p *AudioDevices) Init(ctx *PageCtx) error { } // Get font size from database - baseFontSize := p.Base.Context.CfgPtr.StandardFontSize + baseFontSize := p.Context.CfgPtr.StandardFontSize // Calculate scales titleScale := float32(64) / float32(baseFontSize) @@ -85,7 +84,7 @@ func (p *AudioDevices) Init(ctx *PageCtx) error { } // Get currently selected device from database - currentDevice := p.Base.Context.CfgPtr.SelectedAudioDevice + currentDevice := p.Context.CfgPtr.SelectedAudioDevice // Create UI items for each device for i, name := range devices { // Iterate over the devices @@ -103,7 +102,7 @@ func (p *AudioDevices) Init(ctx *PageCtx) error { } } - on := p.Base.Context.CfgPtr.AudioEnabled + on := p.Context.CfgPtr.AudioEnabled audioStatus := "Turn Audio On" if on { @@ -166,11 +165,11 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error { // Handle audio disable if selection == "Turn Audio Off" || selection == "Turn Audio On" { - newStatus := !p.Base.Context.CfgPtr.AudioEnabled - updatedConfig := p.Base.Context.CfgPtr + newStatus := !p.Context.CfgPtr.AudioEnabled + updatedConfig := p.Context.CfgPtr updatedConfig.AudioEnabled = newStatus - p.Base.Context.DbPtr.Save(&updatedConfig) - p.Base.Context.RefreshConfig() + p.Context.DbPtr.Save(&updatedConfig) + p.Context.RefreshConfig() return nil } @@ -186,12 +185,12 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error { } // Save settings - p.Base.Context.CfgPtr.SelectedAudioDevice = deviceName - p.Base.Context.CfgPtr.AudioEnabled = true - p.Base.Context.DbPtr.Save(p.Base.Context.CfgPtr) - p.Base.Context.RefreshConfig() + p.Context.CfgPtr.SelectedAudioDevice = deviceName + p.Context.CfgPtr.AudioEnabled = true + p.Context.DbPtr.Save(p.Context.CfgPtr) + p.Context.RefreshConfig() case sdl.K_BACKSPACE: - p.Base.Context.NavigateTo(SETTINGS) + p.Context.NavigateTo(SETTINGS) } return nil @@ -202,36 +201,31 @@ func (p *AudioDevices) Render() error { p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color) p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color) - // No devices to display - if len(p.devices) == 0 { - return nil - } - // Render highlight box behind highlighted item - p.RenderSolidColorQuad( - p.solidShader, - p.devices[p.highlightIndex].Position(), - p.devices[p.highlightIndex].RawDimensions(), - colors.White, - ) + //p.RenderSolidColorQuad( + // p.solidShader, + // p.devices[p.highlightIndex].Position(), + // p.devices[p.highlightIndex].RawDimensions(), + // colors.White, + //) // Render device items with appropriate colors - for i, item := range p.devices { - if i == p.highlightIndex && i == p.selectedIndex { - // Currently highlighted AND selected device - dark green - item.Color = colors.DarkGreen - } else if i == p.highlightIndex { - // Just highlighted - black (shows on white background) - item.Color = colors.Black - } else if i == p.selectedIndex { - // Just selected - green - item.Color = colors.Green - } else { - // Normal item - white - item.Color = colors.White - } - p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color) - } + //for i, item := range p.devices { + // if i == p.highlightIndex && i == p.selectedIndex { + // // Currently highlighted AND selected device - dark green + // item.Color = colors.DarkGreen + // } else if i == p.highlightIndex { + // // Just highlighted - black (shows on white background) + // item.Color = colors.Black + // } else if i == p.selectedIndex { + // // Just selected - green + // item.Color = colors.Green + // } else { + // // Normal item - white + // item.Color = colors.White + // } + // p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color) + //} return nil } diff --git a/internal/page/emailAddressPage.go b/internal/page/emailAddressPage.go index c27e20b..3465c09 100644 --- a/internal/page/emailAddressPage.go +++ b/internal/page/emailAddressPage.go @@ -154,9 +154,9 @@ func (p *EmailAddressPage) HandleEvent(event *sdl.Event) error { sdl.StopTextInput() // Save email address to database - p.Base.Context.CfgPtr.EmailAddress = p.inputText - p.Base.Context.DbPtr.Save(p.Base.Context.CfgPtr) - p.Base.Context.RefreshConfig() + p.Context.CfgPtr.EmailAddress = p.inputText + p.Context.DbPtr.Save(p.Context.CfgPtr) + p.Context.RefreshConfig() if p.isFirstTimeSetup { // First-time setup: navigate to License Key page @@ -253,7 +253,7 @@ func (p *EmailAddressPage) Update(dt float32) error { // Render performs page drawing logic. func (p *EmailAddressPage) Render() error { // Get base font size from database - baseFontSize := p.Base.Context.CfgPtr.StandardFontSize + baseFontSize := p.Context.CfgPtr.StandardFontSize sm := shaderManager.Get() diff --git a/internal/page/home.go b/internal/page/home.go index 8122c0f..ed1e669 100644 --- a/internal/page/home.go +++ b/internal/page/home.go @@ -74,7 +74,7 @@ func (p *Home) Init(ctx *PageCtx) error { } // Create license info textures if data exists - if p.Base.Context.CfgPtr.EmailAddress != "" || p.Base.Context.CfgPtr.LicenseKey != "" { + if p.Context.CfgPtr.EmailAddress != "" || p.Context.CfgPtr.LicenseKey != "" { p.hasLicenseInfo = true // Create heading @@ -84,8 +84,8 @@ func (p *Home) Init(ctx *PageCtx) error { } // Create email display if exists - if p.Base.Context.CfgPtr.EmailAddress != "" { - emailText := fmt.Sprintf("Email: %s", p.Base.Context.CfgPtr.EmailAddress) + if p.Context.CfgPtr.EmailAddress != "" { + emailText := fmt.Sprintf("Email: %s", p.Context.CfgPtr.EmailAddress) p.emailDisplay, err = NewStringItem(emailText, font, colors.White) if err != nil { return fmt.Errorf("unable to create email display texture: %w", err) @@ -93,8 +93,8 @@ func (p *Home) Init(ctx *PageCtx) error { } // Create license key display if exists - if p.Base.Context.CfgPtr.LicenseKey != "" { - licenseKeyText := fmt.Sprintf("License Key: %s", p.Base.Context.CfgPtr.LicenseKey) + if p.Context.CfgPtr.LicenseKey != "" { + licenseKeyText := fmt.Sprintf("License Key: %s", p.Context.CfgPtr.LicenseKey) p.licenseKeyDisplay, err = NewStringItem(licenseKeyText, font, colors.White) if err != nil { return fmt.Errorf("unable to create license key display texture: %w", err) @@ -103,7 +103,7 @@ func (p *Home) Init(ctx *PageCtx) error { } // Get base font size from database - baseFontSize := p.Base.Context.CfgPtr.StandardFontSize + baseFontSize := p.Context.CfgPtr.StandardFontSize p.baseFontSize = baseFontSize diff --git a/internal/page/licenseKeyPage.go b/internal/page/licenseKeyPage.go index 8bf1061..d971d47 100644 --- a/internal/page/licenseKeyPage.go +++ b/internal/page/licenseKeyPage.go @@ -77,7 +77,7 @@ func (p *LicenseKeyPage) Init(ctx *PageCtx) error { p.titleTextureHeight = titleHeight // Load existing license key from database - licenseKey := p.Base.Context.CfgPtr.LicenseKey + licenseKey := p.Context.CfgPtr.LicenseKey if licenseKey != "" { p.inputText = licenseKey @@ -165,7 +165,7 @@ func (p *LicenseKeyPage) HandleEvent(event *sdl.Event) error { } // Allow escape back to settings if coming from settings sdl.StopTextInput() - p.Base.Context.NavigateTo(SETTINGS) + p.Context.NavigateTo(SETTINGS) case sdl.K_RETURN: // Validate license key - check empty first, then length, then characters cleaned := strings.ReplaceAll(p.inputText, "-", "") @@ -188,9 +188,9 @@ func (p *LicenseKeyPage) HandleEvent(event *sdl.Event) error { sdl.StopTextInput() // Save license key to database - p.Base.Context.CfgPtr.LicenseKey = p.inputText - p.Base.Context.DbPtr.Save(p.Base.Context.CfgPtr) - p.Base.Context.RefreshConfig() + p.Context.CfgPtr.LicenseKey = p.inputText + p.Context.DbPtr.Save(p.Context.CfgPtr) + p.Context.RefreshConfig() // Write license key to plain text file for the updater if homeDir, err := os.UserHomeDir(); err == nil { @@ -202,10 +202,10 @@ func (p *LicenseKeyPage) HandleEvent(event *sdl.Event) error { if p.isFirstTimeSetup { // First-time setup: rewind to Welcome page - p.Base.Context.NavigateTo(HOME) + p.Context.NavigateTo(HOME) } else { // Coming from settings: rewind back to settings - p.Base.Context.NavigateTo(SETTINGS) + p.Context.NavigateTo(SETTINGS) } case sdl.K_BACKSPACE: if len(p.inputText) > 0 { @@ -303,7 +303,7 @@ func (p *LicenseKeyPage) Update(dt float32) error { // Render performs page drawing logic. func (p *LicenseKeyPage) Render() error { // Get base font size from database - baseFontSize := p.Base.Context.CfgPtr.StandardFontSize + baseFontSize := p.Context.CfgPtr.StandardFontSize sm := shaderManager.Get() diff --git a/internal/page/logViewerPage.go b/internal/page/logViewerPage.go index 7470d21..aa735d5 100644 --- a/internal/page/logViewerPage.go +++ b/internal/page/logViewerPage.go @@ -42,7 +42,7 @@ func (p *LogViewerPage) Init(ctx *PageCtx) error { return fmt.Errorf("failed base page initialization: %w", err) } - if result := p.Base.Context.DbPtr.Order("timestamp desc").Limit(250).Find(&logs); result.Error != nil { + if result := p.Context.DbPtr.Order("timestamp desc").Limit(250).Find(&logs); result.Error != nil { logger.Error("error querying database for logs: %s", result.Error) return fmt.Errorf("%s", result.Error) } @@ -115,7 +115,7 @@ func (p *LogViewerPage) HandleEvent(event *sdl.Event) error { if e.Type == sdl.KEYDOWN { switch e.Keysym.Sym { case sdl.K_BACKSPACE: - p.Base.Context.NavigateTo(SETTINGS) + p.Context.NavigateTo(SETTINGS) case sdl.K_UP: p.scrollOffset -= p.lineHeight if p.scrollOffset < 0 { @@ -159,7 +159,7 @@ func (p *LogViewerPage) Update(dt float32) error { } func (p *LogViewerPage) Render() error { - baseFontSize := p.Base.Context.CfgPtr.StandardFontSize + baseFontSize := p.Context.CfgPtr.StandardFontSize sm := shaderManager.Get() textShader, _ := sm.Get("text") diff --git a/internal/page/message.go b/internal/page/message.go index f05bc7d..43a81d0 100644 --- a/internal/page/message.go +++ b/internal/page/message.go @@ -61,7 +61,7 @@ func (p *MessagePage) Init(ctx *PageCtx) error { return fmt.Errorf("unable to create instructions string texture: %w", err) } - baseFontSize := float32(p.Base.Context.CfgPtr.StandardFontSize) + baseFontSize := float32(p.Context.CfgPtr.StandardFontSize) p.message.Scale(float32(48) / baseFontSize) p.instructions.Scale(float32(28) / baseFontSize) diff --git a/internal/page/scrollerPage.go b/internal/page/scrollerPage.go index 0b39652..02c21cf 100644 --- a/internal/page/scrollerPage.go +++ b/internal/page/scrollerPage.go @@ -231,7 +231,7 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { // Get line pattern from database var err error - linePatternName := p.Base.Context.CfgPtr.LinePattern + linePatternName := p.Context.CfgPtr.LinePattern p.activePattern, err = linePattern.Get(linePatternName) if err != nil { @@ -239,7 +239,7 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { } // Initialize color pattern - var baseColors = []sdl.Color{ + baseColors := []sdl.Color{ colors.Tan, colors.PeachPuff, colors.LightCyan, @@ -252,7 +252,7 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { // Get font sizes from database (optimize: load once, cache as int32) // Match the working page's logic exactly: - p.fontScale = float32(32) / float32(p.Base.Context.CfgPtr.StandardFontSize) + p.fontScale = float32(32) / float32(p.Context.CfgPtr.StandardFontSize) // Font setup nLines := int(float32(p.ScreenHeight) / 32.0) // 32 is your target visual size @@ -291,7 +291,7 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { } // Audio setup - audioDeviceName := p.Base.Context.CfgPtr.SelectedAudioDevice + audioDeviceName := p.Context.CfgPtr.SelectedAudioDevice // Switch audio devices if needed if mixer.CurrentDevice() != audioDeviceName { @@ -302,12 +302,12 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { // Start audio playback - // selectedAudio := p.Base.Context.CfgPtr.SelectedAudio + // selectedAudio := p.Context.CfgPtr.SelectedAudio - assetsDir := p.Base.Context.CfgPtr.AssetsDir + assetsDir := p.Context.CfgPtr.AssetsDir audioFile := filepath.Join(assetsDir, "audio/audio.wav") logger.Info("Attempting to play %s", audioFile) - if err := mixer.Play(audioFile, true, p.Base.Context.CfgPtr); err != nil { + if err := mixer.Play(audioFile, true, p.Context.CfgPtr); err != nil { logger.Error("Mixer returned an error: %v", err) // ADD THIS } @@ -414,15 +414,6 @@ func (p *ScrollerPage) HandleEvent(event *sdl.Event) error { if !p.autoVelocity { p.velocity -= p.increment } - case sdl.K_a: - p.autoVelocity = !p.autoVelocity - if p.autoVelocity { - // Reset to initial state when enabling - p.velocity = p.velocityFloor - p.velocityDirection = 1 - p.velocityState = 0 - p.velocityTimer = 0.0 - } } return nil } @@ -594,7 +585,7 @@ func (p *ScrollerPage) Destroy() error { return nil } - mixer.Stop(1000, p.Base.Context.CfgPtr) + mixer.Stop(1000, p.Context.CfgPtr) // GL cleanup — delete all text textures if p.lines != nil { From 4f33296f24fd83813885e6ccb5570dd1e91e6d54 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Mon, 13 Apr 2026 13:19:46 -0500 Subject: [PATCH 05/22] remove device list audio toggle is still not rendering --- internal/page/audioDevices.go | 162 ++++++++++------------------------ 1 file changed, 45 insertions(+), 117 deletions(-) diff --git a/internal/page/audioDevices.go b/internal/page/audioDevices.go index b4fd897..7b4afde 100644 --- a/internal/page/audioDevices.go +++ b/internal/page/audioDevices.go @@ -8,23 +8,22 @@ import ( "rhemawavetech.com/rhemawave/internal/colors" "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" - "rhemawavetech.com/rhemawave/internal/mixer" + "rhemawavetech.com/rhemawave/internal/logger" "rhemawavetech.com/rhemawave/internal/shaderManager" ) type AudioDevices struct { Base - font *fontManager.FontEntry - textShader *shaderManager.Shader - solidShader *shaderManager.Shader - title StringItem - prompt StringItem - devices []StringItem - highlightIndex int - selectedIndex int - verticalMargin int32 - CfgPtr *db.AppConfig - // DbPtr *gorm.DB + font *fontManager.FontEntry + textShader *shaderManager.Shader + solidShader *shaderManager.Shader + title StringItem + prompt StringItem + AudioStatusToggle StringItem + highlightIndex int + selectedIndex int + verticalMargin int32 + CfgPtr *db.AppConfig } func (p *AudioDevices) Init(ctx *PageCtx) error { @@ -76,32 +75,6 @@ func (p *AudioDevices) Init(ctx *PageCtx) error { } p.prompt.Scale(itemScale) - // Get list of audio output devices - devices := mixer.ListDevices() - if len(devices) == 0 { - // Create a default entry so the page doesn't break - devices = []string{"System Default"} - } - - // Get currently selected device from database - currentDevice := p.Context.CfgPtr.SelectedAudioDevice - - // Create UI items for each device - for i, name := range devices { // Iterate over the devices - item, err := NewStringItem(name, p.font, colors.White) // Create a new string string texture - if err != nil { - return fmt.Errorf("failed to create device item name: %w", err) - } - item.Scale(itemScale) // Scale the new string item - p.devices = append(p.devices, item) // Add the new scaled string item to the list of items - - // Mark currently selected device - if name == currentDevice { - p.selectedIndex = i - p.highlightIndex = i // Start with cursor on current device - } - } - on := p.Context.CfgPtr.AudioEnabled audioStatus := "Turn Audio On" @@ -117,12 +90,26 @@ func (p *AudioDevices) Init(ctx *PageCtx) error { // Scale the new option audioStatusStringItem.Scale(itemScale) - p.devices = append(p.devices, audioStatusStringItem) - p.layout() return nil } +func (p *AudioDevices) CreateAudioStatusItem() error { + on := p.Context.CfgPtr.AudioEnabled + statusString := "Turn Audio Off" // The audio is ON + color := colors.Green + if !on { + statusString = "Turn Audio on" + color = colors.Red + } + audioStatusStringItem, err := NewStringItem(statusString, p.font, color) + if err != nil { + return fmt.Errorf("failed to create '%s' string item: %v", statusString, err) + } + p.AudioStatusToggle = audioStatusStringItem + return nil +} + func (p *AudioDevices) layout() { // Position title at bottom p.title.X = p.ScreenCenterX - p.title.W/2 @@ -131,16 +118,6 @@ func (p *AudioDevices) layout() { // Position prompt above title p.prompt.X = p.ScreenCenterX - p.prompt.W/2 p.prompt.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H - - // Position device items stacked upward from prompt - for i := range p.devices { - p.devices[i].X = p.ScreenCenterX - p.devices[i].W/2 - if i == 0 { - p.devices[i].Y = p.prompt.Y - p.verticalMargin*2 - p.devices[i].H - } else { - p.devices[i].Y = p.devices[i-1].Y - p.devices[i].H - 10 - } - } } func (p *AudioDevices) HandleEvent(event *sdl.Event) error { @@ -150,45 +127,16 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error { } switch e.Keysym.Sym { - case sdl.K_UP: - if p.highlightIndex > 0 { - p.highlightIndex-- - } - - case sdl.K_DOWN: - if p.highlightIndex < len(p.devices)-1 { - p.highlightIndex++ - } case sdl.K_RETURN: - selection := p.devices[p.highlightIndex].content - - // Handle audio disable - if selection == "Turn Audio Off" || selection == "Turn Audio On" { - newStatus := !p.Context.CfgPtr.AudioEnabled - updatedConfig := p.Context.CfgPtr - updatedConfig.AudioEnabled = newStatus - p.Context.DbPtr.Save(&updatedConfig) - p.Context.RefreshConfig() - return nil - } - - // Map "System Default" to empty string for SDL - deviceName := selection - if selection == "System Default" { - deviceName = "" - } - - // Validate by attempting the switch - if err := mixer.SwitchDevice(deviceName); err != nil { - return fmt.Errorf("failed to switch to device %q: %w", selection, err) - } - - // Save settings - p.Context.CfgPtr.SelectedAudioDevice = deviceName - p.Context.CfgPtr.AudioEnabled = true + // Toggle AudioEnabled + p.Context.CfgPtr.AudioEnabled = !p.Context.CfgPtr.AudioEnabled + // Save the config to the database p.Context.DbPtr.Save(p.Context.CfgPtr) + // Refresh the config throughout the application p.Context.RefreshConfig() + // Update AudioEnabled UI element + case sdl.K_BACKSPACE: p.Context.NavigateTo(SETTINGS) } @@ -198,43 +146,23 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error { func (p *AudioDevices) Render() error { // Always render title and prompt - p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color) - p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color) - - // Render highlight box behind highlighted item - //p.RenderSolidColorQuad( - // p.solidShader, - // p.devices[p.highlightIndex].Position(), - // p.devices[p.highlightIndex].RawDimensions(), - // colors.White, - //) - - // Render device items with appropriate colors - //for i, item := range p.devices { - // if i == p.highlightIndex && i == p.selectedIndex { - // // Currently highlighted AND selected device - dark green - // item.Color = colors.DarkGreen - // } else if i == p.highlightIndex { - // // Just highlighted - black (shows on white background) - // item.Color = colors.Black - // } else if i == p.selectedIndex { - // // Just selected - green - // item.Color = colors.Green - // } else { - // // Normal item - white - // item.Color = colors.White - // } - // p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color) - //} - + if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil { + logger.Error("unable to render title texture: %v", err) + return fmt.Errorf("unable to render title texture: %w", err) + } + if err := p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color); err != nil { + logger.Error("unable to render prompt texture: %v", err) + return fmt.Errorf("unable to render prompt texture: %w", err) + } + if err := p.RenderTexture(p.textShader, p.AudioStatusToggle.ID, p.AudioStatusToggle.Position(), p.AudioStatusToggle.RawDimensions(), p.AudioStatusToggle.Color); err != nil { + logger.Error("unable to render audioStatusToggle texture: %v", err) + return fmt.Errorf("unable to render audioStatusToggle texture: %w", err) + } return nil } func (p *AudioDevices) Destroy() error { gl.DeleteTextures(1, &p.title.ID) gl.DeleteTextures(1, &p.prompt.ID) - for _, item := range p.devices { - gl.DeleteTextures(1, &item.ID) - } return nil } From 105d8900d58d58e098eaa93ac8f30e48091b8f83 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Mon, 13 Apr 2026 14:00:02 -0500 Subject: [PATCH 06/22] Replace audio devices with audio toggle --- .vscode/launch.json | 18 ++++++++++ internal/page/audioDevices.go | 63 ++++++++++++++++++----------------- 2 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2a88165 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch file", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "rhemawave.go", + "env": { + "DEBUGINFOD_URLS": "" + } + }, + ] +} \ No newline at end of file diff --git a/internal/page/audioDevices.go b/internal/page/audioDevices.go index 7b4afde..1fa117e 100644 --- a/internal/page/audioDevices.go +++ b/internal/page/audioDevices.go @@ -20,9 +20,8 @@ type AudioDevices struct { title StringItem prompt StringItem AudioStatusToggle StringItem - highlightIndex int - selectedIndex int verticalMargin int32 + ItemScale float32 CfgPtr *db.AppConfig } @@ -59,37 +58,26 @@ func (p *AudioDevices) Init(ctx *PageCtx) error { // Calculate scales titleScale := float32(64) / float32(baseFontSize) - itemScale := float32(32) / float32(baseFontSize) + p.ItemScale = float32(32) / float32(baseFontSize) // Create title - p.title, err = NewStringItem("Select an Audio Output Device", p.font, colors.White) + p.title, err = NewStringItem("Toggle Audio", p.font, colors.White) if err != nil { return fmt.Errorf("creating title: %w", err) } p.title.Scale(titleScale) // Create prompt - p.prompt, err = NewStringItem("Use Arrow Keys to select, Enter to confirm", p.font, colors.White) + p.prompt, err = NewStringItem("Switch On/Off with Enter. Setting saves immediately.", p.font, colors.White) if err != nil { return fmt.Errorf("creating prompt: %w", err) } - p.prompt.Scale(itemScale) + p.prompt.Scale(p.ItemScale) - on := p.Context.CfgPtr.AudioEnabled - - audioStatus := "Turn Audio On" - if on { - audioStatus = "Turn Audio Off" + if err = p.CreateAudioStatusItem(); err != nil { + logger.Error("failed to create AudioStatusItem in AudioDevices.Init()") } - audioStatusStringItem, err := NewStringItem(audioStatus, p.font, colors.White) - if err != nil { - return fmt.Errorf("failed to create Turn Audio Off string item: %w", err) - } - - // Scale the new option - audioStatusStringItem.Scale(itemScale) - p.layout() return nil } @@ -106,20 +94,40 @@ func (p *AudioDevices) CreateAudioStatusItem() error { if err != nil { return fmt.Errorf("failed to create '%s' string item: %v", statusString, err) } + fmt.Printf("AudioStatusToggle (%d)", audioStatusStringItem.ID) + audioStatusStringItem.Scale(p.ItemScale) p.AudioStatusToggle = audioStatusStringItem + + // Position the audio toggle + p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2 // Horizontal Position + p.AudioStatusToggle.Y = p.ScreenCenterY + (p.AudioStatusToggle.H / 2) // Vertical Position return nil } func (p *AudioDevices) layout() { - // Position title at bottom + // Position title at the top p.title.X = p.ScreenCenterX - p.title.W/2 p.title.Y = p.ScreenHeight - p.title.H - p.verticalMargin*2 - // Position prompt above title + // Position prompt below the title p.prompt.X = p.ScreenCenterX - p.prompt.W/2 p.prompt.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H } +func (p *AudioDevices) ToggleAudio() { + // Toggle current setting + p.Context.CfgPtr.AudioEnabled = !p.Context.CfgPtr.AudioEnabled + + // Update the database + p.Context.DbPtr.Save(p.Context.CfgPtr) + + // Update the configuration + p.Context.RefreshConfig() + + // Update AudioEnabled UI element + p.CreateAudioStatusItem() +} + func (p *AudioDevices) HandleEvent(event *sdl.Event) error { e, ok := (*event).(*sdl.KeyboardEvent) if !ok || e.Type != sdl.KEYDOWN { @@ -129,14 +137,7 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error { switch e.Keysym.Sym { case sdl.K_RETURN: - // Toggle AudioEnabled - p.Context.CfgPtr.AudioEnabled = !p.Context.CfgPtr.AudioEnabled - // Save the config to the database - p.Context.DbPtr.Save(p.Context.CfgPtr) - // Refresh the config throughout the application - p.Context.RefreshConfig() - // Update AudioEnabled UI element - + p.ToggleAudio() case sdl.K_BACKSPACE: p.Context.NavigateTo(SETTINGS) } @@ -145,19 +146,21 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error { } func (p *AudioDevices) Render() error { - // Always render title and prompt if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil { logger.Error("unable to render title texture: %v", err) return fmt.Errorf("unable to render title texture: %w", err) } + if err := p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color); err != nil { logger.Error("unable to render prompt texture: %v", err) return fmt.Errorf("unable to render prompt texture: %w", err) } + if err := p.RenderTexture(p.textShader, p.AudioStatusToggle.ID, p.AudioStatusToggle.Position(), p.AudioStatusToggle.RawDimensions(), p.AudioStatusToggle.Color); err != nil { logger.Error("unable to render audioStatusToggle texture: %v", err) return fmt.Errorf("unable to render audioStatusToggle texture: %w", err) } + return nil } From 4f392242c9c3bf684ee4202a31be0704f736d3b3 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Mon, 13 Apr 2026 14:01:11 -0500 Subject: [PATCH 07/22] Update audio page name --- internal/page/settingsPage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/page/settingsPage.go b/internal/page/settingsPage.go index 7d0d6e3..66466e4 100644 --- a/internal/page/settingsPage.go +++ b/internal/page/settingsPage.go @@ -87,7 +87,7 @@ func (p *Settings) Init(ctx *PageCtx) error { label: wifiLabel, }) - audioLabel, err := NewStringItem("Audio Device", font, colors.White) + audioLabel, err := NewStringItem("Audio", font, colors.White) if err != nil { return fmt.Errorf("creating audio label: %w", err) } From 76fd6057778b361619411fafb8e9464c42a853ce Mon Sep 17 00:00:00 2001 From: NeilClack Date: Mon, 13 Apr 2026 14:09:26 -0500 Subject: [PATCH 08/22] Update changelog --- tasks.md => changelog.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) rename tasks.md => changelog.md (67%) diff --git a/tasks.md b/changelog.md similarity index 67% rename from tasks.md rename to changelog.md index 57302a4..3cb27fd 100644 --- a/tasks.md +++ b/changelog.md @@ -1,3 +1,22 @@ +# Changelog +Until release, any items missing "." indicate something I have not talked to Mr. Gouldd about. + +## [Unreleased] + +### Added +### Changed +- [X] Changed audio devices page to be strictly an audio on/off toggle. +### Deprecated +### Removed +- [X] Removed list of audio devices from audio device page +### Fixed +### Security + +--- + +# Scratch Pad +> Just a place to keep notes until I make proper changelog items. + - [\-] Audio device does not always initialize when starting scroller. - [ ] Each proclamation should play for between 3 and 5 minutes before continuing to the next. - [ ] Proclamations should be downloaded again when the Proclamation Options page is opened. @@ -12,8 +31,9 @@ where items missing from the database are removed from the application. - [ ] Investigate playing video from the application, try using SDL. - [ ] WiFi screen has a bug causing users to believe they entered the wrong WiFi password when they have in-fact connected to their WiFi successfully. -- [ ] Rotate the screen 90 degrees on demand. +- [\?] Rotate the screen 90 degrees on demand. - [ ] Force audio to always use HDMI connection. +- [X] Disable audio device selection for only audio on/off switch - [ ] Create subscription tiers and block proclamations from being selected. - [ ] Create schedules to play specific playlists of proclamations. - [ ] Download Mr. Gouldd's music choices. @@ -21,4 +41,7 @@ connected to their WiFi successfully. - [ ] Fix the Attic server. - [ ] Update deployed kiosks to use the new attic server. This will cause a rebuild on the machines but I think that is okay. Rebuild issues were during ISO development, not kiosk development. -- [ ] All pages should share a font and font manager. Font Manager should be loaded prior to creating any pages. +- [ ] All pages should share a font and font manager. Font Manager should be loaded prior to creating any pages. + +*?* Cage does not allow rotating the display. This should likely be done in software, but I need to check NixOS configuration first. +To avoid doing this within RhemaWave would avoid many headaches, bugs, and a LOT of code. \ No newline at end of file From 32648377cee0e468860ee8d554188d6d9e8c5295 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 15 Apr 2026 10:47:48 -0500 Subject: [PATCH 09/22] commit --- .claude/settings.local.json | 24 ---------------- changelog.md | 57 +++++++++++-------------------------- 2 files changed, 17 insertions(+), 64 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 28bc551..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(cat:*)", - "Bash(git checkout:*)", - "Bash(dpkg:*)", - "Bash(chmod:*)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(./build.sh:*)", - "Bash(ls:*)", - "Bash(umask)", - "Bash(namei:*)", - "Bash(cd /home/nclack/Work/Rhema\\\\ Wave/RhemaWave && go list:*)", - "Bash(go mod download:*)", - "Bash(CGO_ENABLED=1 go build:*)", - "Bash(go vet:*)", - "Bash(go version:*)", - "Bash(dlv version:*)", - "Bash(sudo nixos-rebuild:*)", - "Bash(nix develop:*)" - ] - } -} diff --git a/changelog.md b/changelog.md index 3cb27fd..5ec1f02 100644 --- a/changelog.md +++ b/changelog.md @@ -1,47 +1,24 @@ -# Changelog +# Changelog + Until release, any items missing "." indicate something I have not talked to Mr. Gouldd about. -## [Unreleased] +## [Unreleased] -### Added -### Changed -- [X] Changed audio devices page to be strictly an audio on/off toggle. -### Deprecated -### Removed -- [X] Removed list of audio devices from audio device page -### Fixed -### Security +### Added ---- +### Changed -# Scratch Pad -> Just a place to keep notes until I make proper changelog items. +- [x] Changed audio devices page to be strictly an audio on/off toggle. -- [\-] Audio device does not always initialize when starting scroller. -- [ ] Each proclamation should play for between 3 and 5 minutes before continuing to the next. -- [ ] Proclamations should be downloaded again when the Proclamation Options page is opened. -- [ ] Proclamation downloads should be a sync, not a complete overwrite, and should handle cases -where items missing from the database are removed from the application. -- [ ] Real license keys still need to be implemented. The functionality exists to check them, but they are not checked. -- [ ] Migrate from SDL2 to SDL3. -- [ ] Migrate from OpenGL Core to OpenGL GLES for compatibility on ARM devices. -- [ ] Prevent switching screens during "pre-run" checks such as WiFi, Email, and License Key. -- [ ] TOS pages need to be re-added. -- [ ] Create a short tutorial of how to use the application. -- [ ] Investigate playing video from the application, try using SDL. -- [ ] WiFi screen has a bug causing users to believe they entered the wrong WiFi password when they have in-fact -connected to their WiFi successfully. -- [\?] Rotate the screen 90 degrees on demand. -- [ ] Force audio to always use HDMI connection. -- [X] Disable audio device selection for only audio on/off switch -- [ ] Create subscription tiers and block proclamations from being selected. -- [ ] Create schedules to play specific playlists of proclamations. -- [ ] Download Mr. Gouldd's music choices. -- [ ] Use the fileserver to obtain audio upon installation, not packaging it all in the binary. -- [ ] Fix the Attic server. -- [ ] Update deployed kiosks to use the new attic server. This will cause a rebuild on the machines -but I think that is okay. Rebuild issues were during ISO development, not kiosk development. -- [ ] All pages should share a font and font manager. Font Manager should be loaded prior to creating any pages. +### Deprecated + +### Removed + +- [x] Removed list of audio devices from audio device page + +### Fixed + +### Security + +--- -*?* Cage does not allow rotating the display. This should likely be done in software, but I need to check NixOS configuration first. -To avoid doing this within RhemaWave would avoid many headaches, bugs, and a LOT of code. \ No newline at end of file From b6bf0f1873b75d4e7698817926709d8b623ab927 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 15 Apr 2026 11:01:28 -0500 Subject: [PATCH 10/22] Sync proclamations when entering proclamation list --- internal/application/application.go | 4 ++-- internal/page/proclamationOptions.go | 5 ++++- internal/sync/sync.go | 13 ++++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/application/application.go b/internal/application/application.go index a725d94..e77e332 100644 --- a/internal/application/application.go +++ b/internal/application/application.go @@ -154,8 +154,8 @@ func Run() { }) // Sync the proclamations from the API to the Database - sync.SyncProclamations(apiURL, DB) - sync.SyncCategories(apiURL, DB) + sync.SyncProclamations(DB) + sync.SyncCategories(DB) app.runEventLoop() } diff --git a/internal/page/proclamationOptions.go b/internal/page/proclamationOptions.go index 6719c0b..e57f6b7 100644 --- a/internal/page/proclamationOptions.go +++ b/internal/page/proclamationOptions.go @@ -15,6 +15,7 @@ import ( "rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/logger" "rhemawavetech.com/rhemawave/internal/shaderManager" + "rhemawavetech.com/rhemawave/internal/sync" ) // Scrollbar Visuals @@ -78,10 +79,12 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error { p.allItems = []*FlatItem{} p.scrollOffset = 0 p.isSearchActive = false - p.scrollTrackWidth = 30 p.scrollTrackX = p.ScreenWidth - p.scrollTrackWidth + // Syncronize proclamations first + sync.SyncProclamations(ctx.DbPtr) + fm := fontManager.Get() font, _ := fm.GetFont("Roboto-Regular") p.font = font diff --git a/internal/sync/sync.go b/internal/sync/sync.go index d9b5fd0..ce375cf 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -1,7 +1,6 @@ package sync import ( - "fmt" "regexp" "gorm.io/gorm" @@ -14,8 +13,9 @@ var htmlRegex = regexp.MustCompile(`<[^>]*>`) // SyncProclamations fetches data from the API, cleans it, and updates the local DB. // It returns the slice of proclamations so the UI can update immediately. -func SyncProclamations(apiURL string, database *gorm.DB) ([]db.Proclamation, error) { - fullURL := fmt.Sprintf("%s/proclamations", apiURL) +func SyncProclamations(database *gorm.DB) ([]db.Proclamation, error) { + fullURL := "https://portal.rhemawavetech.com/api/v1/proclamations" + logger.Debug("Syncronizing proclamations. URL: %s", fullURL) // 1. Fetch from API proclamations, err := netreq.GetJSON[db.Proclamation](fullURL) @@ -25,6 +25,7 @@ func SyncProclamations(apiURL string, database *gorm.DB) ([]db.Proclamation, err logger.Error("Proclamation sync failed, skipping update: %v", err) return nil, err } + logger.Debug("Request Granted") // 2. Data Transformation (Cleaning) for i := range proclamations { @@ -40,6 +41,8 @@ func SyncProclamations(apiURL string, database *gorm.DB) ([]db.Proclamation, err return nil }) + logger.Debug("Proclamations stored to database") + if err != nil { logger.Error("Failed to save synced proclamations: %v", err) return nil, err @@ -49,8 +52,8 @@ func SyncProclamations(apiURL string, database *gorm.DB) ([]db.Proclamation, err return proclamations, nil } -func SyncCategories(apiURL string, database *gorm.DB) ([]db.ProclamationCategory, error) { - fullURL := fmt.Sprintf("%s/categories", apiURL) +func SyncCategories(database *gorm.DB) ([]db.ProclamationCategory, error) { + fullURL := "https://portal.rhemawavetech.com/api/v1/categories" categories, err := netreq.GetJSON[db.ProclamationCategory](fullURL) if err != nil { logger.Error("Category sync failed, skipping update: %v", err) From 6b358bae42bacd4144e6c34b84d9703260f2f52c Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 15 Apr 2026 13:44:21 -0500 Subject: [PATCH 11/22] Justify settings entries --- internal/page/settingsPage.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/page/settingsPage.go b/internal/page/settingsPage.go index 66466e4..9cb665d 100644 --- a/internal/page/settingsPage.go +++ b/internal/page/settingsPage.go @@ -161,12 +161,19 @@ func (p *Settings) Init(ctx *PageCtx) error { p.title.Y = p.ScreenHeight - p.title.H - p.verticalMargin*2 // Position prompt above title - p.prompt.X = p.ScreenCenterX - p.prompt.W/2 + p.prompt.X = p.ScreenCenterX - int32(float32(p.prompt.W)/2) p.prompt.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H + // If this value starts at 0, nothing is further left than that... + var furthestLeft int32 = p.ScreenWidth // < more to the left, > more to the right, = same + // Position options stacked upward from prompt for i, option := range p.options { - p.options[i].label.X = p.ScreenCenterX - option.label.W/2 + p.options[i].label.X = p.ScreenCenterX - option.label.W/2 // Calculate horizontal center posision + if p.options[i].label.X < furthestLeft { // If the left most pixel is further left than furthestLeft, indicating a longer line: + // Then update furthestLeft + furthestLeft = p.options[i].label.X + } if i == 0 { p.options[i].label.Y = int32(float32(p.prompt.Y)-(float32(p.verticalMargin)*4)) - option.label.H } else { @@ -174,6 +181,11 @@ func (p *Settings) Init(ctx *PageCtx) error { } } + // Now, re-iterate over the options and justify everything to furthestLeft + for i := range p.options { + p.options[i].label.X = furthestLeft + } + return nil } From 9c88f0c9fdfba378d2f8a3d10d9c2fba97579130 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 15 Apr 2026 13:45:30 -0500 Subject: [PATCH 12/22] Re-organize Settings Menu --- internal/page/settingsPage.go | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/internal/page/settingsPage.go b/internal/page/settingsPage.go index 9cb665d..e1ec7ed 100644 --- a/internal/page/settingsPage.go +++ b/internal/page/settingsPage.go @@ -78,15 +78,6 @@ func (p *Settings) Init(ctx *PageCtx) error { label: proclamationsLabel, }) - wifiLabel, err := NewStringItem("WiFi Setup", font, colors.White) - if err != nil { - return fmt.Errorf("creating wifi label: %w", err) - } - p.options = append(p.options, Option{ - target: WIFI, - label: wifiLabel, - }) - audioLabel, err := NewStringItem("Audio", font, colors.White) if err != nil { return fmt.Errorf("creating audio label: %w", err) @@ -114,13 +105,13 @@ func (p *Settings) Init(ctx *PageCtx) error { label: licenseKeyLabel, }) - logsLabel, err := NewStringItem("Logs", font, colors.White) + wifiLabel, err := NewStringItem("WiFi Setup", font, colors.White) if err != nil { - return fmt.Errorf("creating logs label: %w", err) + return fmt.Errorf("creating wifi label: %w", err) } p.options = append(p.options, Option{ - target: LOGS, - label: logsLabel, + target: WIFI, + label: wifiLabel, }) updateLabel, err := NewStringItem("Check for Updates", font, colors.White) @@ -132,6 +123,18 @@ func (p *Settings) Init(ctx *PageCtx) error { label: updateLabel, }) + logsLabel, err := NewStringItem("Logs", font, colors.White) + if err != nil { + return fmt.Errorf("creating logs label: %w", err) + } + p.options = append(p.options, Option{ + target: LOGS, + label: logsLabel, + }) + + // TODO: Add entry for "Tutorial" + // TODO: Add entry for "About" + // Get base font size from database baseFontSize := p.Context.CfgPtr.StandardFontSize From c8756d052816166fb69852f04cef26ecb2b7f5f1 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 15 Apr 2026 11:50:03 -0500 Subject: [PATCH 13/22] Recurse if inited but no deviceOpen --- internal/mixer/mixer.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/mixer/mixer.go b/internal/mixer/mixer.go index 65da5d4..da7dff9 100644 --- a/internal/mixer/mixer.go +++ b/internal/mixer/mixer.go @@ -113,8 +113,11 @@ func Play(path string, loopForever bool, cfg *db.AppConfig) error { defer mu.Unlock() // Check if mixer has been initialized and that a device is open - // JUMP: This is where the issue is... if !inited || !deviceOpen { + if !deviceOpen { + SwitchDevice("") // Re-switch to the default device + Play(path, loopForever, cfg) // Re-run the function + } logger.Error("audio mixer not initialized") return errors.New("audio mixer not initialized") } From 10ba5df132615abeca39b770dc5fa7bca04e79de Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 15 Apr 2026 11:58:44 -0500 Subject: [PATCH 14/22] Force re-initialization of audio device when Play() is called --- internal/mixer/mixer.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/internal/mixer/mixer.go b/internal/mixer/mixer.go index da7dff9..f143680 100644 --- a/internal/mixer/mixer.go +++ b/internal/mixer/mixer.go @@ -390,13 +390,13 @@ func SwitchDevice(newName string) error { } // Log device switch attempt - oldDevice := deviceName + // Automatically defaults to "system default" + oldDevice := CurrentDevice() // Which device is to be closed if oldDevice == "" { oldDevice = "system default" } - newDeviceLog := newName - if newDeviceLog == "" { - newDeviceLog = "system default" + if newName == "" { // Which device is being switched to? + newName = "system default" } // Capture current state @@ -421,11 +421,15 @@ func SwitchDevice(newName string) error { mix.CloseAudio() deviceOpen = false } + + // Attempt to open the newName device if err := openDeviceLocked(newName); err != nil { - // Best effort: try to reopen the previous device (or default) so we don't leave audio dead + // If error switching to new device, switch back to the old device if restoreErr := openDeviceLocked(oldDevice); restoreErr != nil { - } else { + // We should not hit an error when switching back, but if we do... + logger.Error("Failed to switch to device: %s", oldDevice) } + // Could not switch to the new device, return the error return err } @@ -451,6 +455,8 @@ func SwitchDevice(newName string) error { } } + logger.Info("Audio device switched to %s", newName) + return nil } From b94279be691a8082f60bc8da321f9f900c77068f Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 15 Apr 2026 13:56:53 -0500 Subject: [PATCH 15/22] Update changelog --- changelog.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index 5ec1f02..8124dd0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,22 +1,26 @@ # Changelog -Until release, any items missing "." indicate something I have not talked to Mr. Gouldd about. - ## [Unreleased] ### Added +- Added proclamation sync when entering "proclamations" settings page + ### Changed -- [x] Changed audio devices page to be strictly an audio on/off toggle. +- Changed audio devices page to be strictly an audio on/off toggle +- Changed settings menu items to be left-aligned with eachother +- Changed settings entry organization ### Deprecated ### Removed -- [x] Removed list of audio devices from audio device page +- Removed list of audio devices from audio device page -### Fixed +### Fixed + +- Fixed audio device not being initialized on first entry to F2 screen ### Security From 504095caa8261e0ebec7d9482a023c759bbf8152 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 17 Apr 2026 11:43:59 -0500 Subject: [PATCH 16/22] Sync instead of download. Updated Sync functions to perform proper synchronizations instead of pure downloads. This means that if something is deleted on the server, it will be deleted on the client as well, instead of just left hanging. The SyncProclamations function was also updated to perform this way, but while retaining the user's saved list. --- internal/sync/sync.go | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/internal/sync/sync.go b/internal/sync/sync.go index ce375cf..7b09a4a 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -17,7 +17,6 @@ func SyncProclamations(database *gorm.DB) ([]db.Proclamation, error) { fullURL := "https://portal.rhemawavetech.com/api/v1/proclamations" logger.Debug("Syncronizing proclamations. URL: %s", fullURL) - // 1. Fetch from API proclamations, err := netreq.GetJSON[db.Proclamation](fullURL) if err != nil { // If we're offline or API is down, we log it but don't crash. @@ -27,17 +26,37 @@ func SyncProclamations(database *gorm.DB) ([]db.Proclamation, error) { } logger.Debug("Request Granted") - // 2. Data Transformation (Cleaning) for i := range proclamations { proclamations[i].Content = htmlRegex.ReplaceAllString(proclamations[i].Content, "") } - // 3. Persist to Database - // Using Transaction ensures we don't end up with partial data if something fails + var storedProclamationSettings []db.Proclamation + database.Select("ID", "Selected").Find(&storedProclamationSettings) + // [ID]Selected + stash := make(map[string]bool) + for _, row := range storedProclamationSettings { + stash[row.ID] = row.Selected + } + + for i := range proclamations { + proclamations[i].Selected = stash[proclamations[i].ID] + } + err = database.Transaction(func(tx *gorm.DB) error { - if err := tx.Omit("selected").Save(&proclamations).Error; err != nil { + // Clear the database + // Where "1 = 1" because batch Delete() requires a condition + // 'Where "1 = 1"' is like saying "while true be true" and tricks GORM into deleting everything + if err := tx.Where("1 = 1").Delete(db.Proclamation{}).Error; err != nil { return err } + // Then re-populate the database + if err := tx.Save(&proclamations).Error; err != nil { + return err + } + // Old sync code + // if err := tx.Omit("selected").Save(&proclamations).Error; err != nil { + // return err + // } return nil }) @@ -59,12 +78,22 @@ func SyncCategories(database *gorm.DB) ([]db.ProclamationCategory, error) { logger.Error("Category sync failed, skipping update: %v", err) return nil, err } + err = database.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("1 = 1").Delete(&db.ProclamationCategory{}).Error; err != nil { + return err + } if err := tx.Save(&categories).Error; err != nil { return err } return nil }) + + if err != nil { + logger.Error("failed to sync proclamation categories: %v", err) + return nil, err + } + logger.Info("Successfully synced proclamation categories: %d", len(categories)) return categories, nil } From 82651ec49141b27ad215c27a4a40e03a561ac946 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Mon, 20 Apr 2026 13:11:30 -0500 Subject: [PATCH 17/22] Remove 'Check for Updates' entry in Settings --- internal/page/settingsPage.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/internal/page/settingsPage.go b/internal/page/settingsPage.go index e1ec7ed..23cee0a 100644 --- a/internal/page/settingsPage.go +++ b/internal/page/settingsPage.go @@ -114,15 +114,6 @@ func (p *Settings) Init(ctx *PageCtx) error { label: wifiLabel, }) - updateLabel, err := NewStringItem("Check for Updates", font, colors.White) - if err != nil { - return fmt.Errorf("creating update label: %w", err) - } - p.options = append(p.options, Option{ - target: CHECK_UPDATES, - label: updateLabel, - }) - logsLabel, err := NewStringItem("Logs", font, colors.White) if err != nil { return fmt.Errorf("creating logs label: %w", err) From 89b3a3b6d5fb74c5f453e86d25aafc4c6a7fe531 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 21 Apr 2026 15:19:42 -0500 Subject: [PATCH 18/22] Shorten version Customer's don't care about these updates, just minor/major, so why confuse them? --- internal/page/home.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/page/home.go b/internal/page/home.go index ed1e669..f6b127e 100644 --- a/internal/page/home.go +++ b/internal/page/home.go @@ -12,7 +12,7 @@ import ( "rhemawavetech.com/rhemawave/internal/shaderManager" ) -var GitVersion = "v1.0.0" +var GitVersion = "v1.0" type Home struct { Base @@ -30,7 +30,6 @@ type Home struct { // Init performs basic page initialization and creation of static assets. func (p *Home) Init(ctx *PageCtx) error { - title := "Rhema Wave" version := GitVersion prompt := "Press F2 to Start, or F3 for Settings" From 0197f6cba8850bcbea584b45ea25b81c506a4661 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 21 Apr 2026 15:22:24 -0500 Subject: [PATCH 19/22] Cleanup version by removing the 'v' --- internal/page/home.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/page/home.go b/internal/page/home.go index f6b127e..535dcd0 100644 --- a/internal/page/home.go +++ b/internal/page/home.go @@ -12,7 +12,7 @@ import ( "rhemawavetech.com/rhemawave/internal/shaderManager" ) -var GitVersion = "v1.0" +var GitVersion = "1.0" type Home struct { Base From dd069e934508d00b45c9b324999ba8bba630435b Mon Sep 17 00:00:00 2001 From: NeilClack Date: Thu, 23 Apr 2026 16:44:09 -0500 Subject: [PATCH 20/22] WIP: Stagger proclamations Proclamations are now inserted 10 times, and in between each proclamation, the Universal Wellness proclamation is inserted. Now I need to optimize the loop & remove the Universal Wellbeing option from the Proclamations menu (hide it) because it's always enabled. --- internal/page/scrollerPage.go | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/page/scrollerPage.go b/internal/page/scrollerPage.go index 02c21cf..c5ab2d1 100644 --- a/internal/page/scrollerPage.go +++ b/internal/page/scrollerPage.go @@ -74,6 +74,12 @@ import ( "rhemawavetech.com/rhemawave/internal/shaderManager" ) +const ( + defaultProclamationID string = "f3d42354-3197-45e4-9b08-be447f14386d" + proclamationPlayCount int = 10 + defaultProclamationPlayCount int = 10 +) + type Line struct { words []StringItem width int32 @@ -330,13 +336,32 @@ func (p *ScrollerPage) loadProclamations() error { return err } + var defaultProclamation db.Proclamation + err = p.Context.DbPtr.Where("ID = ?", defaultProclamationID).Find(&defaultProclamation).Error + if err != nil { + return err + } + // Build word list from selected, available proclamations + // For each proclamation in proclamations + // until count reaches 10 + // add the words into the world list + // update count + // when the count reaches 10 + // insert the defaultProclamation + // reset the count to 0 + var count int = 0 for _, proclamation := range proclamations { - if proclamation.Selected { - // Split content into words and add to list + for count < 10 { + logger.Debug("Adding %s", proclamation.Name) words := strings.Fields(proclamation.Content) p.words = append(p.words, words...) + count += 1 } + logger.Debug("Adding %s", defaultProclamation.Name) + defaultWords := strings.Fields(defaultProclamation.Content) + p.words = append(p.words, defaultWords...) + count = 0 } if len(p.words) == 0 { From cfd67f3545476442cf151831ea4eb77573061152 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Thu, 23 Apr 2026 16:52:12 -0500 Subject: [PATCH 21/22] Optimize word collection loop --- internal/page/scrollerPage.go | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/internal/page/scrollerPage.go b/internal/page/scrollerPage.go index c5ab2d1..5476bb6 100644 --- a/internal/page/scrollerPage.go +++ b/internal/page/scrollerPage.go @@ -343,25 +343,21 @@ func (p *ScrollerPage) loadProclamations() error { } // Build word list from selected, available proclamations - // For each proclamation in proclamations - // until count reaches 10 - // add the words into the world list - // update count - // when the count reaches 10 - // insert the defaultProclamation - // reset the count to 0 - var count int = 0 + defaultWords := strings.Fields(defaultProclamation.Content) + for _, proclamation := range proclamations { - for count < 10 { + words := strings.Fields(proclamation.Content) + + // Pre-allocate: 10x proclamation words + 1x default words + batch := make([]string, 0, len(words)*10+len(defaultWords)) + for range 10 { logger.Debug("Adding %s", proclamation.Name) - words := strings.Fields(proclamation.Content) - p.words = append(p.words, words...) - count += 1 + batch = append(batch, words...) } logger.Debug("Adding %s", defaultProclamation.Name) - defaultWords := strings.Fields(defaultProclamation.Content) - p.words = append(p.words, defaultWords...) - count = 0 + batch = append(batch, defaultWords...) + + p.words = append(p.words, batch...) } if len(p.words) == 0 { From ec3ccabb624a86330975e8e6c3c2b559a6834e32 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Thu, 23 Apr 2026 17:10:56 -0500 Subject: [PATCH 22/22] Remove Universal Wellbeing from visual and toggle-able list --- internal/page/proclamationOptions.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/page/proclamationOptions.go b/internal/page/proclamationOptions.go index e57f6b7..61343ff 100644 --- a/internal/page/proclamationOptions.go +++ b/internal/page/proclamationOptions.go @@ -108,19 +108,26 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error { // Load current selections var selected []db.Proclamation - p.Context.DbPtr.Where("selected = ?", true).Find(&selected) + p.Context.DbPtr.Where("selected = ?", true).Where("id != ?", defaultProclamationID).Find(&selected) for _, s := range selected { + if s.ID == defaultProclamationID { + logger.Debug("Found default proclamation in selected list: %s", s.ID) + } p.activeMap[s.ID] = true } // Load ALL proclamations into a lookup map for SearchText population var allProcs []db.Proclamation - if err := p.Context.DbPtr.Find(&allProcs).Error; err != nil { + if err := p.Context.DbPtr.Where("id != ?", defaultProclamationID).Find(&allProcs).Error; err != nil { logger.Error("Failed to fetch proclamations: %s", err) return err } + procContentMap := make(map[string]string, len(allProcs)) for _, pr := range allProcs { + if pr.ID == defaultProclamationID { + logger.Debug("Found default proclamation in allProcs list: %s", pr.ID) + } procContentMap[pr.ID] = pr.Content } p.allProclamations = []*FlatItem{} @@ -245,7 +252,7 @@ func (p *ProclamationOptions) expand(index int) { // Load proclamations var procs []db.Proclamation - p.Context.DbPtr.Where("category_id = ?", parentID).Find(&procs) + p.Context.DbPtr.Where("category_id = ?", parentID).Where("ID != ?", defaultProclamationID).Find(&procs) for _, pr := range procs { var fi *FlatItem // Reuse existing proclamation from allProclamations if available @@ -487,8 +494,9 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error { p.acceptChanges() case sdl.K_SPACE: item := p.allItems[p.highlightIndex] - if !item.IsCategory { - p.activeMap[item.ID] = !p.activeMap[item.ID] + if !item.IsCategory { // If the item is NOT a Category + p.activeMap[item.ID] = !p.activeMap[item.ID] // Toggle it from the 'selected' list + logger.Debug("Select/Deselect Proclamation: %s %s", item.Label, item.ID) } else if !item.IsSubCategory { p.toggleExpansion(p.highlightIndex) }