Initial release: RadiantWave v1.0

This commit is contained in:
NeilClack
2026-03-12 08:05:10 -05:00
commit 403ec4db89
555 changed files with 437856 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
{
"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:*)"
]
}
}
+1
View File
@@ -0,0 +1 @@
use flake
+45
View File
@@ -0,0 +1,45 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
__debug*
# Project files used as symlinks for development
settings.json
logs/
# Build output
bin/
*.pprof
*.log
# Audio assets (large binary files)
share/rhemawave/audio/*.wav
# Nix
.direnv/
result
notes.txt
+45
View File
@@ -0,0 +1,45 @@
# Rhema Wave Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## IMPORTANT!
`9cdef97` - This commit hash is considered the "Great Reset Point".
Many bugs, issues, and complexities have been discovered, leading to a restructure
of the project, packaging, deployment and even target machine environment.
All tags prior to this commit have been deleted. All releases removed, all archived
packages dropped from the server, and even the server structure has been changed.
v0.9.0 will represent the "beta" release from this point forward. Until full release, we will
remain at v0.9.x.
## [Unreleased]
### Added
- TOS screens now display real content from the RWT Welcome Screen document (10 sections)
- TOS content is stored in the database and seeded from HTML files on disk, making it easy to update without recompiling
- WiFi setup page now appears automatically before TOS screens if no internet connection is detected
- Dev script `setup.sh` for syncing assets to /usr/ on development machines
---
## [v0.9.22]
### Added
- Batched texture rendering path (BeginTextureRendering/RenderTextureBatched/EndTextureRendering) for ScrollerPage
- Ring buffer (LineDeque) for ScrollerPage line management — O(1) prepend/append/remove
- In-memory config value cache in db package — eliminates repeated SQLite queries
- Buffered channel-based logger — single writer goroutine replaces per-log goroutine spawning
- Cached uniform locations on Shader struct — looked up once at load, reused every frame
### Changed
- FontManager now reuses a persistent FBO, VAO, and VBO for string texture rendering instead of creating/deleting per call
- RenderSolidColorQuad uses cached uniform locations and removes per-frame glGetError calls
- Welcome page caches init_font_size at Init instead of querying the database every frame
- ScrollerPage.Destroy uses gl.Flush instead of gl.Finish to avoid blocking CPU on page transitions
- Scroll offset calculation uses simple subtraction instead of math.Mod with float64 round-trip
- Application cleanup now properly shuts down the logger goroutine via logger.Close()
### Removed
- Removed per-frame CheckGLError calls from RenderSolidColorQuad (caused GPU pipeline stalls)
+201
View File
@@ -0,0 +1,201 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
RhemaWave is a Go-based fullscreen SDL2/OpenGL application designed to run on Linux kiosk systems. It displays proclamations with audio playback and animated visual patterns. The application uses a page-based navigation system and includes network connectivity validation, license key management, and an auto-update mechanism.
## Build & Development
### Development Workflow
```bash
nix develop # enter dev shell (provides Go, SDL2, etc.)
go build -o bin/rhemawave rhemawave.go # build binary
./bin/rhemawave # run (requires display server + audio)
./dev_scripts/reset.sh # wipe local database and start fresh
```
The binary at `bin/rhemawave` resolves assets via `../share/rhemawave` relative to itself, so the repo layout (`bin/` + `share/rhemawave/`) works out of the box.
### Nix Build
```bash
nix build # produces ./result/bin/rhemawave + ./result/share/rhemawave/
```
The flake's `postInstall` copies `share/` into `$out/`, preserving the same relative layout.
### Testing
There are currently no automated tests in this codebase.
## Architecture
### Application Entry Point
- `rhemawave.go` is the main entry point, calls `internal/application.Run()`
- `internal/application/application.go` contains the core application lifecycle
### Core Application Flow
1. **Initialization**
- Create application data directory (`~/.local/share/rhemawave`)
- Initialize SQLite database (`data.db`) with GORM
- Initialize SDL2 subsystems (VIDEO, AUDIO)
- Initialize SDL_ttf for font rendering
- Create fullscreen OpenGL 3.3 Core Profile window
- Initialize shader and font managers
- Hide cursor and enable relative mouse mode
2. **Validation Phase** (startup page stack, top → bottom)
- WiFi setup page (if no internet connection detected) — shown first
- TOS sections from database (unaccepted only, ordered by section number)
- Email address page (if missing)
- License key page (if missing)
- Welcome page — always at bottom
- License key is also validated against the API
3. **Event Loop** (60 FPS target)
- Poll SDL events
- Handle keyboard events via keybinds system
- Update current page with delta time
- Render current page
- Apply pending page transitions
### Page System
Pages implement the `page.Page` interface:
- `Init(app ApplicationInterface) error` - Setup resources
- `HandleEvent(event *sdl.Event) error` - Handle input
- `Update(dt float32) error` - Update state
- `Render() error` - Draw to screen
- `Destroy() error` - Cleanup resources
Pages can embed `page.Base` for common functionality (OpenGL quad rendering, projection matrix, screen dimensions).
**Page Navigation:**
- `PushPage(p)` - Add page to stack (creates history)
- `SwitchPage(p)` - Replace current page
- `UnwindToPage(p)` - Pop back to a specific page (or create if not in stack)
**Available Pages:**
- `Welcome` - Initial screen showing version
- `ScrollerPage` - Main proclamation player (F2)
- `Settings` - Settings menu (F3)
- `WiFiSetupPage` - Network configuration
- `LicenseKeyPage` - License key entry
- `EmailAddressPage` - Email address entry
- `AudioDevices` - Audio device selection
- `ProclamationOptions` - Proclamation selection
- `TOSPage` - Terms of Service acceptance screen (scrollable, Y/N to accept/decline)
### Key Global Keybinds
Defined in `application.initKeybinds()`:
- `F1` - Navigate to Welcome page
- `F2` - Navigate to Player (ScrollerPage)
- `F3` - Navigate to Settings
- `SHIFT+UP` - Increase volume
- `SHIFT+DOWN` - Decrease volume
- `RIGHT_CTRL+Q` - Quit application
### Package Structure
**`internal/application`** - Core application lifecycle, event loop, page management, validation
**`internal/page`** - Page interface and all page implementations
**`internal/db`** - Database layer using GORM with SQLite
- Models: `Config`, `Proclamations`, `Log`, `TOSSection`
- Manages configuration key-value pairs
- Seeds default proclamations from `share/rhemawave/proclamations/`
- Seeds TOS sections from `share/rhemawave/tos/*.html` (filename format: `{NN} - {Title}.html`)
**`internal/audio`** - SDL2 audio subsystem integration (currently minimal wrapper)
**`internal/mixer`** - Audio playback manager for music and proclamations
**`internal/graphics`** - OpenGL utilities and display orientation handling
**`internal/shaderManager`** - Loads and manages OpenGL shaders from `share/rhemawave/shaders/`
**`internal/fontManager`** - Loads and manages TrueType fonts from `share/rhemawave/fonts/`
**`internal/keybinds`** - Global keyboard shortcut registration and dispatch
**`internal/network`** - Network connectivity manager (uses D-Bus for NetworkManager on Linux)
**`internal/logger`** - Logging system that writes to database and stdout
**`internal/colors`** - Color utilities
**`internal/pattern`** - Visual pattern generation (line patterns, color patterns)
### Repository Layout
```
bin/ # build output (gitignored)
rhemawave
share/rhemawave/ # assets (checked in)
audio/ # Audio files (audio.wav, etc.)
fonts/ # TrueType fonts
shaders/ # GLSL shader files
proclamations/ # Proclamation text files
home/ # Non-commercial proclamations
commercial/ # Commercial proclamations
tos/ # TOS HTML files (seeded into DB on first run)
packaging/ # deployment configs (kiosk, updater, polkit)
kiosk/ # kiosk system configs
rhemawave-updater.py # auto-updater script
internal/ # Go source packages
dev_scripts/
reset.sh # deletes ~/.local/share/rhemawave/
flake.nix # Nix build and dev shell
```
### Important Implementation Details
**Asset Resolution:** `resolveAssetsDir()` computes `filepath.Join(filepath.Dir(exe), "..", "share", "rhemawave")` — works in both dev (`bin/rhemawave` → `share/rhemawave/`) and Nix install (`$out/bin/rhemawave` → `$out/share/rhemawave/`).
**OpenGL Context:** Uses OpenGL 3.3 Core Profile with VSync enabled. The page base class sets up a reusable quad VAO/VBO for texture and solid color rendering.
**Database Location:** `~/.local/share/rhemawave/data.db` - Contains configuration, proclamations, and log entries
**Fullscreen Mode:** Uses `SDL_WINDOW_FULLSCREEN_DESKTOP` to match native display resolution
**Display Orientation:** Handled by `graphics.SetDisplayOrientation()` which swaps width/height for portrait displays
**Version Injection:** Git version is injected at build time via ldflags: `-X 'rhemawavetech.com/rhemawave/internal/page.GitVersion=${VERSION}'`
**Network Manager:** Uses D-Bus to communicate with NetworkManager for WiFi configuration and connectivity status
## Development Workflow
### Adding a New Page
1. Create `internal/page/<pagename>.go`
2. Implement the `page.Page` interface
3. Embed `page.Base` for common functionality
4. Register keybind in `application.initKeybinds()` if needed
5. Add navigation logic from existing pages
### Modifying Database Schema
1. Update models in `internal/db/db.go`
2. GORM AutoMigrate will handle schema changes on next run
3. Add seed data logic if needed in `ensureDefaultsExist()`, `seedDefaultProclamations()`, or `seedDefaultTOSSections()`
### Adding New Assets
1. Place files in appropriate `share/rhemawave/` subdirectory
2. They will be included automatically in Nix builds via `postInstall`
3. Access via config values (`assets_dir`) or direct paths
### Working with Shaders
1. Create `.vert` and `.frag` files in `share/rhemawave/shaders/`
2. Load via `shaderManager.GetShader("<name>")`
3. Shader must have uniforms: `u_mvpMatrix`, `u_color` or `u_textColor`, `u_texture` (if using textures)
## Important Constraints
- Must build with `CGO_ENABLED=1` for SDL2 bindings
- Target platform is Linux (uses D-Bus for NetworkManager)
- Application expects to run as kiosk user with polkit rules for system operations
- Assets are resolved relative to the binary (`../share/rhemawave`), so `bin/` and `share/` must be siblings
- Version string comes from git tags (release) or commit hash (dev)
- ShaderManager, FontManager, and Logger use `sync.Once` singletons — they must be initialized (via their `Init()` or `Initialize()` functions) before calling `Get()`; calling `Get()` before initialization panics
- Deployed via NixOS-Anywhere to kiosk hardware
+21
View File
@@ -0,0 +1,21 @@
# 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
+47
View File
@@ -0,0 +1,47 @@
# RhemaWave
A Go-based fullscreen SDL2/OpenGL application for Linux kiosk systems. Displays proclamations with audio playback and animated visual patterns.
## Development
Development uses Nix for a reproducible build environment.
```bash
nix develop # enter dev shell
go build -o bin/rhemawave rhemawave.go # build
./bin/rhemawave # run
./dev_scripts/reset.sh # reset database (if needed)
```
### Prerequisites
- Nix with flakes enabled
- A display server (X11/Wayland) and audio device for running the application
## Architecture
- **Page-based UI** — SDL2/OpenGL 3.3 fullscreen rendering with a page stack for navigation
- **Proclamation player** — scrolling text display with synchronized audio playback
- **Startup validation** — WiFi, TOS acceptance, email, license key checks
- **Singleton managers** — ShaderManager, FontManager, Logger (initialized once via `sync.Once`)
### Key Directories
```
bin/ # build output (gitignored)
share/rhemawave/ # assets (fonts, shaders, proclamations, TOS)
internal/ # Go source packages
packaging/ # deployment configs (kiosk, updater, polkit)
dev_scripts/ # developer utilities
```
### Runtime Data
Stored in `~/.local/share/rhemawave/`:
- `data.db` — SQLite database (config, proclamations, logs)
- `log.txt` — application log
- `license_key` — license key file
## Deployment
Deployed to kiosk hardware via NixOS-Anywhere. See `packaging/` for kiosk system configs.
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
rm -rf ~/.local/share/rhemawave/
echo "Data directory deleted, application reset."
+67
View File
@@ -0,0 +1,67 @@
# 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
+189
View File
@@ -0,0 +1,189 @@
# 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 <<EOF
#%PAM-1.0
auth required pam_unix.so
auth required pam_env.so
account required pam_unix.so
password required pam_unix.so
session required pam_unix.so
session required pam_limits.so
EOF
```
### 6. Configure Greetd Auto-Login
Create `/etc/greetd/config.toml`:
```bash
sudo tee /etc/greetd/config.toml <<EOF
[terminal]
vt = 1
[initial_session]
command = "cage -s -- /usr/local/bin/rhemawave"
user = "kiosk"
[default_session]
command = "cage -s -- /usr/local/bin/rhemawave"
user = "kiosk"
EOF
```
### 7. Disable Network Wait Service
Speed up boot by disabling network wait:
```bash
sudo systemctl disable systemd-networkd-wait-online.service
sudo systemctl mask systemd-networkd-wait-online.service
```
## Install RhemaWave
### Option 1: Via APT Repository (Recommended)
```bash
# Add repository
echo "deb [trusted=yes] https://repository.rhemawavetech.com/release ./" | \
sudo tee /etc/apt/sources.list.d/rhemawave.list
# Install
sudo apt update
sudo apt install rhemawave
```
### Option 2: Manual Installation
```bash
# Download and install .deb package
wget https://repository.rhemawavetech.com/rhemawave_X.X.X_amd64.deb
sudo dpkg -i rhemawave_X.X.X_amd64.deb
sudo apt install -f
```
## Enable and Start Greetd
```bash
sudo systemctl enable greetd
sudo systemctl start greetd
```
## Verify Setup
Check greetd status:
```bash
sudo systemctl status greetd
```
Check logs if issues occur:
```bash
journalctl -xeu greetd
```
## Reboot
```bash
sudo reboot
```
The system should auto-login as `kiosk` and launch RhemaWave in fullscreen via cage.
## Troubleshooting
### Greetd fails to start
- Check PAM config: `cat /etc/pam.d/greetd`
- Check greetd config syntax: `cat /etc/greetd/config.toml`
- View detailed logs: `journalctl -xeu greetd`
### Black screen after login
- Verify cage is installed: `which cage`
- Verify rhemawave exists: `ls -l /usr/local/bin/rhemawave`
- Check cage can access GPU: `ls -l /dev/dri/`
- Add kiosk to video group: `sudo usermod -aG video kiosk`
### Network not connecting
- Check NetworkManager status: `systemctl status NetworkManager`
- Use nmtui for WiFi setup: `sudo nmtui`
- RhemaWave will show WiFi setup page if no connection detected
### Auto-updates not working
- Verify polkit rules: `ls -l /etc/polkit-1/rules.d/99-rhemawave-updater.rules`
- Check updater script: `ls -l /usr/local/bin/rhemawave-updater.py`
- Test manual update: `sudo apt update && sudo apt list --upgradable`
## Security Notes
- The kiosk user keeps their password for SSH and sudo access
- Auto-login only applies to tty1 (the primary console)
- Other TTYs (Ctrl+Alt+F2-F6) still require password
- Polkit rules restrict kiosk user to only RhemaWave package updates
## Remote Management
### SSH Access
```bash
ssh kiosk@<kiosk-ip>
```
### 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
```
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1771848320,
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+98
View File
@@ -0,0 +1,98 @@
{
description = "RhemaWave";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
(flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfreePredicate = pkg: (nixpkgs.lib.getName pkg) == "rhemawave";
};
buildInputs = with pkgs; [
SDL2
SDL2_ttf
SDL2_image
SDL2_mixer
libGL
sqlite
dbus
wayland
wayland-protocols
libxkbcommon
libdecor
libx11
libxext
];
nativeBuildInputs = with pkgs; [
go_1_24
pkg-config
];
audioFile = pkgs.fetchurl {
url = "https://files.neilclack.com/audio.wav";
sha256 = "sha256-5Dikxx9RKOoPfY1ukIgMoVQ7tzpMirp+XPj6OphfEW0=";
};
in
{
packages.default = pkgs.buildGoModule {
pname = "rhemawave";
version = "v1.0.0";
src = ./.;
vendorHash = null;
inherit buildInputs nativeBuildInputs;
env.CGO_ENABLED = "1";
preBuild = ''
export CGO_CFLAGS="$(pkg-config --cflags SDL2_mixer SDL2_ttf SDL2_image) $CGO_CFLAGS"
export CGO_LDFLAGS="$(pkg-config --libs SDL2_mixer SDL2_ttf SDL2_image) $CGO_LDFLAGS"
'';
ldflags = [ "-s" "-w" ];
postInstall = ''
cp -r share $out/share
mkdir -p $out/share/rhemawave/audio
cp ${audioFile} $out/share/rhemawave/audio/audio.wav
'';
meta = with pkgs.lib; {
description = "RhemaWave kiosk application";
license = licenses.unfree;
platforms = platforms.linux;
};
};
devShells.default = pkgs.mkShell {
inherit buildInputs nativeBuildInputs;
shellHook = ''
export OLD_PS1="$PS1"
export PS1="[nix-env] $OLD_PS1"
export CGO_CFLAGS="$(pkg-config --cflags SDL2_mixer SDL2_ttf SDL2_image) $CGO_CFLAGS"
export CGO_LDFLAGS="$(pkg-config --libs SDL2_mixer SDL2_ttf SDL2_image) $CGO_LDFLAGS"
echo "RhemaWave dev shell"
echo " go version: $(go version)"
echo " CGO_ENABLED=1"
echo ""
echo "Build: go build -o bin/rhemawave rhemawave.go"
echo "Run: ./bin/rhemawave"
echo "Reset: ./dev_scripts/reset.sh"
'';
};
}
)) // {
nixosModules.default = import ./nixos/module.nix self;
};
}
+24
View File
@@ -0,0 +1,24 @@
module rhemawavetech.com/rhemawave
go 1.24.0
toolchain go1.24.2
require github.com/veandco/go-sdl2 v0.4.40
require github.com/go-gl/mathgl v1.2.0
require (
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71
github.com/godbus/dbus/v5 v5.1.0
github.com/google/uuid v1.6.0
)
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
golang.org/x/text v0.31.0 // indirect
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
+29
View File
@@ -0,0 +1,29 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/mathgl v1.2.0 h1:v2eOj/y1B2afDxF6URV1qCYmo1KW08lAMtTbOn3KXCY=
github.com/go-gl/mathgl v1.2.0/go.mod h1:pf9+b5J3LFP7iZ4XXaVzZrCle0Q/vNpB/vDe5+3ulRE=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2 h1:eM10bFtI4UvibIsKr10/QT7Yfz+NADfjZYh0GKrXUNc=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2/go.mod h1:mF2UmIpBnzFeBdu/ypTDb/LdbS0nk0dfSN1WUsWTjMA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U=
github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
+694
View File
@@ -0,0 +1,694 @@
package application
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sync/atomic"
"time"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/graphics"
"rhemawavetech.com/rhemawave/internal/keybinds"
"rhemawavetech.com/rhemawave/internal/license"
"rhemawavetech.com/rhemawave/internal/logger"
"rhemawavetech.com/rhemawave/internal/mixer"
"rhemawavetech.com/rhemawave/internal/network"
"rhemawavetech.com/rhemawave/internal/page"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
const (
targetFPS = 60.0
volumeStepSize = 16
openGLMajorVersion = 3
openGLMinorVersion = 3
apiURL = "http://localhost:8000"
apiPath = "/api/v1/licenseKey/validate"
audioFileURL = "https://files.neilclack.com/audio.wav"
)
type Application struct {
Window *sdl.Window
License *license.Response
pageStack []page.Page
pendingAction func()
running bool
}
// ----------------------------------------------------------------------------
// Main Entry Point
// ----------------------------------------------------------------------------
func Run() {
logger.Info("Starting application")
appDataDir, err := initializeApplicationDirectory()
if err != nil {
logger.Fatal("Failed to initialize application directory: %v", err)
}
logger.Info("App directory: %s", appDataDir)
logger.AddFile(filepath.Join(appDataDir, "log.txt"))
if err := initializeDatabase(appDataDir); err != nil {
logger.Fatal("Database initialization failed: %v", err)
}
app := &Application{}
app.initializeSDLAndOpenGL()
defer app.cleanup()
app.initializeManagers()
app.initializeAudio()
app.validateLicenseKey()
app.startupPageStack()
app.runEventLoop()
}
// ----------------------------------------------------------------------------
// Initialization Functions
// ----------------------------------------------------------------------------
func initializeApplicationDirectory() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
appDataDir := filepath.Join(homeDir, ".local", "share", "rhemawave")
if err := os.MkdirAll(appDataDir, 0755); err != nil {
return "", fmt.Errorf("could not create application data directory %s: %w", appDataDir, err)
}
testFile := filepath.Join(appDataDir, ".access_test")
f, err := os.Create(testFile)
if err != nil {
return "", fmt.Errorf("application data directory %s is not writable: %w", appDataDir, err)
}
f.Close()
os.Remove(testFile)
return appDataDir, nil
}
func initializeDatabase(appDataDir string) error {
dbPath := filepath.Join(appDataDir, "data.db")
if err := db.InitDatabase(dbPath); err != nil {
return fmt.Errorf("failed to initialize database: %w", err)
}
logger.AddDatabase(db.DB)
return nil
}
type progressWriter struct {
w io.Writer
written *atomic.Int64
}
func (pw *progressWriter) Write(p []byte) (int, error) {
n, err := pw.w.Write(p)
pw.written.Add(int64(n))
return n, err
}
// ensureAudioFile checks whether the selected audio file exists. If it does,
// it returns nil. Otherwise it starts a background download and returns a
// DownloadProgressPage that tracks the download so it can be pushed onto the
// page stack.
func ensureAudioFile() *page.DownloadProgressPage {
assetsDir, err := db.GetConfigValue("assets_dir")
if err != nil {
logger.Error("Failed to get assets_dir: %v", err)
return nil
}
selectedAudio, err := db.GetConfigValue("selected_audio")
if err != nil || selectedAudio == "" {
logger.Warning("No selected_audio configured")
return nil
}
audioPath := filepath.Join(assetsDir, selectedAudio)
if _, err := os.Stat(audioPath); err == nil {
logger.Info("Audio file found: %s", audioPath)
return nil
}
logger.Info("Audio file not found at %s, downloading from %s", audioPath, audioFileURL)
totalBytes := &atomic.Int64{}
bytesWritten := &atomic.Int64{}
done := &atomic.Bool{}
hasError := &atomic.Bool{}
downloadPage := &page.DownloadProgressPage{
TotalBytes: totalBytes,
BytesWritten: bytesWritten,
Done: done,
HasError: hasError,
}
go func() {
defer done.Store(true)
if err := os.MkdirAll(filepath.Dir(audioPath), 0755); err != nil {
logger.Error("Failed to create audio directory: %v", err)
hasError.Store(true)
return
}
resp, err := http.Get(audioFileURL)
if err != nil {
logger.Error("Failed to download audio file: %v", err)
hasError.Store(true)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Error("Audio download returned status %d", resp.StatusCode)
hasError.Store(true)
return
}
if resp.ContentLength > 0 {
totalBytes.Store(resp.ContentLength)
}
tmp := audioPath + ".tmp"
f, err := os.Create(tmp)
if err != nil {
logger.Error("Failed to create audio file: %v", err)
hasError.Store(true)
return
}
pw := &progressWriter{w: f, written: bytesWritten}
_, err = io.Copy(pw, resp.Body)
f.Close()
if err != nil {
os.Remove(tmp)
logger.Error("Failed to write audio file: %v", err)
hasError.Store(true)
return
}
if err := os.Rename(tmp, audioPath); err != nil {
os.Remove(tmp)
logger.Error("Failed to move audio file into place: %v", err)
hasError.Store(true)
return
}
logger.Info("Audio file downloaded successfully (%d bytes)", bytesWritten.Load())
}()
return downloadPage
}
func (app *Application) initializeSDLAndOpenGL() {
logger.Info("Initializing SDL")
if err := sdl.Init(sdl.INIT_VIDEO | sdl.INIT_AUDIO); err != nil {
logger.Fatal("SDL init failed: %v", err)
}
logger.Info("Initializing TTF")
if err := ttf.Init(); err != nil {
logger.Fatal("TTF init failed: %v", err)
}
sdl.GLSetAttribute(sdl.GL_CONTEXT_MAJOR_VERSION, openGLMajorVersion)
sdl.GLSetAttribute(sdl.GL_CONTEXT_MINOR_VERSION, openGLMinorVersion)
sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
sdl.ShowCursor(sdl.DISABLE)
sdl.SetRelativeMouseMode(true)
logger.Info("Querying display mode")
displayMode, err := sdl.GetDisplayMode(0, 0)
if err != nil {
logger.Fatal("Display mode query failed: %v", err)
}
if displayMode.W == 0 || displayMode.H == 0 {
logger.Fatal("Invalid display dimensions: %dx%d", displayMode.W, displayMode.H)
}
logger.Info("Display: %dx%d @ %dHz", displayMode.W, displayMode.H, displayMode.RefreshRate)
logger.Info("Creating window")
window, err := sdl.CreateWindow(
"Rhema Wave",
sdl.WINDOWPOS_UNDEFINED,
sdl.WINDOWPOS_UNDEFINED,
displayMode.W,
displayMode.H,
sdl.WINDOW_OPENGL|sdl.WINDOW_FULLSCREEN_DESKTOP|sdl.WINDOW_SHOWN,
)
if err != nil {
logger.Fatal("Window creation failed: %v", err)
}
app.Window = window
logger.Info("Creating OpenGL context")
if _, err := app.Window.GLCreateContext(); err != nil {
logger.Fatal("OpenGL context creation failed: %v", err)
}
logger.Info("Initializing go-gl")
if err := gl.InitWithProcAddrFunc(sdl.GLGetProcAddress); err != nil {
logger.Fatal("go-gl initialization failed: %v", err)
}
logger.Info("OpenGL version: %s", gl.GoStr(gl.GetString(gl.VERSION)))
logger.Info("OpenGL renderer: %s", gl.GoStr(gl.GetString(gl.RENDERER)))
if err := sdl.GLSetSwapInterval(1); err != nil {
logger.Warning("Failed to enable vsync: %v", err)
}
w, h := app.Window.GLGetDrawableSize()
logger.Info("Drawable size: %dx%d", w, h)
logger.Info("Initializing virtual framebuffer (%dx%d -> %dx%d)",
graphics.VirtualWidth, graphics.VirtualHeight, w, h)
if err := graphics.InitVirtualFramebuffer(w, h); err != nil {
logger.Fatal("Virtual framebuffer init failed: %v", err)
}
app.initKeybinds()
logger.Info("SDL and OpenGL initialization complete")
}
func (app *Application) initializeManagers() {
logger.Info("Initializing ShaderManager")
if err := shaderManager.InitShaderManager(); err != nil {
logger.Fatal("ShaderManager initialization failed: %v", err)
}
logger.Info("Initializing FontManager")
if err := fontManager.InitFontManager(); err != nil {
logger.Fatal("FontManager initialization failed: %v", err)
}
}
func (app *Application) initializeAudio() {
logger.Info("Initializing audio")
audioDevice, err := db.GetConfigValue("audio_device_name")
if err != nil {
logger.Warning("Failed to get audio device name from config: %v", err)
}
if err := mixer.Init(audioDevice); err != nil {
logger.Error("Unable to initialize audio device '%s': %v", audioDevice, err)
logger.Error("Audio will be unavailable until a device is configured")
} else {
logger.Info("Audio initialized with device: '%s'", audioDevice)
}
}
func (app *Application) cleanup() {
logger.Info("Cleaning up")
for _, p := range app.pageStack {
p.Destroy()
}
app.pageStack = nil
graphics.DestroyVirtualFramebuffer()
if app.Window != nil {
app.Window.Destroy()
}
ttf.Quit()
sdl.Quit()
logger.Info("Cleanup complete")
logger.Close()
}
// ----------------------------------------------------------------------------
// Event Loop
// ----------------------------------------------------------------------------
func (app *Application) runEventLoop() {
if len(app.pageStack) == 0 {
logger.Fatal("No initial page loaded, pageStack is empty")
}
const frameDelay = time.Second / targetFPS
app.running = true
lastTime := time.Now()
logger.Info("Entering event loop")
for app.running {
frameStart := time.Now()
app.handleEvents()
if !app.running {
break
}
deltaTime := float32(time.Since(lastTime).Seconds())
lastTime = time.Now()
app.updateCurrentPage(deltaTime)
app.renderFrame()
app.applyPendingAction()
frameTime := time.Since(frameStart)
if frameTime < frameDelay {
time.Sleep(frameDelay - frameTime)
}
}
logger.Info("Event loop exited")
}
func (app *Application) handleEvents() {
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch e := event.(type) {
case *sdl.QuitEvent:
app.running = false
return
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN && e.Repeat == 0 {
isCtrlPressed := (e.Keysym.Mod&sdl.KMOD_CTRL) != 0 || (e.Keysym.Mod&sdl.KMOD_GUI) != 0
if e.Keysym.Sym == sdl.K_c && isCtrlPressed {
continue
}
keybinds.PerformAction(e)
}
}
if currentPage := app.currentPage(); currentPage != nil {
if err := currentPage.HandleEvent(&event); err != nil {
logger.Error("Page event handler error: %v", err)
}
}
}
}
func (app *Application) updateCurrentPage(deltaTime float32) {
if currentPage := app.currentPage(); currentPage != nil {
currentPage.Update(deltaTime)
}
}
func (app *Application) renderFrame() {
graphics.BeginVirtualRender()
gl.ClearColor(0.0, 0.0, 0.0, 1.0)
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
if currentPage := app.currentPage(); currentPage != nil {
currentPage.Render()
}
graphics.BlitToScreen()
app.Window.GLSwap()
}
// ----------------------------------------------------------------------------
// Keybind Registration
// ----------------------------------------------------------------------------
func (app *Application) initKeybinds() {
keybinds.Register(sdl.K_F3, sdl.KMOD_NONE, func() {
app.PushPage(&page.Settings{})
})
keybinds.Register(sdl.K_F2, sdl.KMOD_NONE, func() {
downloadPage := ensureAudioFile()
if downloadPage != nil {
downloadPage.NextPage = &page.ScrollerPage{}
app.PushPage(downloadPage)
} else {
app.PushPage(&page.ScrollerPage{})
}
})
keybinds.Register(sdl.K_F1, sdl.KMOD_NONE, func() {
app.UnwindToPage(&page.Welcome{})
})
keybinds.Register(sdl.K_q, sdl.KMOD_RCTRL, func() {
app.Stop()
})
keybinds.Register(sdl.K_UP, sdl.KMOD_SHIFT, func() {
mixer.SetVolume128(volumeStepSize)
})
keybinds.Register(sdl.K_DOWN, sdl.KMOD_SHIFT, func() {
mixer.SetVolume128(-volumeStepSize)
})
}
func (app *Application) Stop() {
app.running = false
}
// ----------------------------------------------------------------------------
// Page Management
// ----------------------------------------------------------------------------
func (app *Application) currentPage() page.Page {
if len(app.pageStack) == 0 {
return nil
}
return app.pageStack[len(app.pageStack)-1]
}
func (app *Application) applyPendingAction() {
if app.pendingAction != nil {
app.pendingAction()
app.pendingAction = nil
}
}
func (app *Application) schedulePush(p page.Page) {
app.pendingAction = func() {
if err := p.Init(app); err != nil {
logger.Error("Failed to init page on push: %v", err)
return
}
if topPage := app.currentPage(); topPage != nil {
topPage.Destroy()
}
app.pageStack = append(app.pageStack, p)
}
}
func (app *Application) scheduleSwitch(p page.Page) {
app.pendingAction = func() {
if topPage := app.currentPage(); topPage != nil {
topPage.Destroy()
app.pageStack = app.pageStack[:len(app.pageStack)-1]
}
if err := p.Init(app); err != nil {
logger.Error("Failed to init page on switch: %v", err)
return
}
app.pageStack = append(app.pageStack, p)
}
}
func (app *Application) scheduleUnwind(targetPage page.Page) {
app.pendingAction = func() {
foundIndex := -1
for i := len(app.pageStack) - 1; i >= 0; i-- {
if app.pageStack[i] == targetPage {
foundIndex = i
break
}
}
if foundIndex != -1 {
for i := len(app.pageStack) - 1; i > foundIndex; i-- {
app.pageStack[i].Destroy()
}
app.pageStack = app.pageStack[:foundIndex+1]
} else {
if topPage := app.currentPage(); topPage != nil {
topPage.Destroy()
app.pageStack = app.pageStack[:len(app.pageStack)-1]
}
if err := targetPage.Init(app); err != nil {
logger.Error("Failed to init page on unwind: %v", err)
return
}
app.pageStack = append(app.pageStack, targetPage)
}
}
}
func (app *Application) SchedulePop() {
app.pendingAction = func() {
if len(app.pageStack) == 0 {
return
}
topPage := app.pageStack[len(app.pageStack)-1]
topPage.Destroy()
app.pageStack = app.pageStack[:len(app.pageStack)-1]
if newTop := app.currentPage(); newTop != nil {
if err := newTop.Init(app); err != nil {
logger.Error("Failed to re-init page after pop: %v", err)
}
}
}
}
// ----------------------------------------------------------------------------
// Public Page Navigation Interface
// ----------------------------------------------------------------------------
func (app *Application) PushPage(p page.Page) {
app.schedulePush(p)
}
func (app *Application) SwitchPage(p page.Page) {
app.scheduleSwitch(p)
}
func (app *Application) UnwindToPage(p page.Page) {
app.scheduleUnwind(p)
}
func (app *Application) GetDrawableSize() (w, h int32) {
return graphics.VirtualWidth, graphics.VirtualHeight
}
func (app *Application) PopPage() {
app.SchedulePop()
}
// ----------------------------------------------------------------------------
// License Validation
// ----------------------------------------------------------------------------
func (app *Application) validateLicenseKey() {
logger.Info("Validating license key")
validator := license.NewValidator(apiURL, apiPath)
key, err := db.GetConfigValue("license_key")
if err != nil {
logger.Error("Failed to fetch license key from database: %v", err)
}
email, err := db.GetConfigValue("email_address")
if err != nil {
logger.Error("Failed to fetch email from database: %v", err)
}
result, err := validator.Validate(key, email)
if err != nil {
logger.Error("License validation failed: %v", err)
app.License = nil
return
}
logger.Info("License validation successful")
app.License = result
}
// ----------------------------------------------------------------------------
// Startup Page Stack
// ----------------------------------------------------------------------------
func (app *Application) startupPageStack() {
app.PushPage(&page.Welcome{})
app.applyPendingAction()
key, _ := db.GetConfigValue("license_key")
if key == "" {
logger.Info("No license key found, pushing license key page")
app.PushPage(&page.LicenseKeyPage{})
app.applyPendingAction()
} else {
// Ensure the license key file exists for the updater script.
// Older versions only stored the key in the database.
if homeDir, err := os.UserHomeDir(); err == nil {
keyPath := filepath.Join(homeDir, ".local", "share", "rhemawave", "license_key")
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
logger.Info("License key file missing, writing from database")
if err := os.MkdirAll(filepath.Dir(keyPath), 0755); err != nil {
logger.Error("Failed to create license key directory: %v", err)
} else if err := os.WriteFile(keyPath, []byte(key), 0644); err != nil {
logger.Error("Failed to write license key file: %v", err)
}
}
}
}
email, _ := db.GetConfigValue("email_address")
if email == "" {
logger.Info("No email address found, pushing email address page")
app.PushPage(&page.EmailAddressPage{})
app.applyPendingAction()
}
// TOS pages — must be accepted before anything else.
// Pushed last so they sit on top of the stack (shown first).
sections, err := db.GetTOSSections()
if err != nil {
logger.Error("Failed to load TOS sections: %v", err)
}
// Push in reverse order so section 1 is on top of the stack
for i := len(sections) - 1; i >= 0; i-- {
s := sections[i]
configKey := fmt.Sprintf("tos_section_%d_accepted", s.SectionNumber)
accepted, _ := db.GetConfigValue(configKey)
if accepted != "true" {
app.PushPage(&page.TOSPage{
Title: s.Title,
Content: s.Content,
ConfigKey: configKey,
})
app.applyPendingAction()
}
}
// Internet check — pushed last so it appears first if no internet.
// Skip WiFi setup if ethernet is connected (it auto-configures) or if
// internet is already reachable through any interface.
nm, err := network.New()
if err != nil {
logger.Warning("Could not initialize network manager: %v", err)
} else {
status, err := nm.CheckInternetConnection()
if err != nil {
logger.Warning("Network connectivity check failed: %v", err)
} else {
switch status.StatusCode {
case network.StatusEthernetConnectedInternetUp,
network.StatusEthernetConnectedNoInternet,
network.StatusWifiConnectedInternetUp:
// Internet is up or ethernet is connected and will auto-configure
default:
logger.Info("No internet connection, pushing WiFi setup page")
app.PushPage(&page.WiFiSetupPage{})
app.applyPendingAction()
}
}
}
}
func (app *Application) Quit() {
logger.Info("User has quit the application")
app.running = false
}
+192
View File
@@ -0,0 +1,192 @@
// Package sdlcolors provides a comprehensive palette of 256 standard colors
// for use with the Go SDL2 bindings. Each color is a public variable of
// type sdl.Color, making them easily accessible for rendering tasks.
package colors
import "github.com/veandco/go-sdl2/sdl"
// Basic Colors
var (
White = sdl.Color{R: 255, G: 255, B: 255, A: 255}
Silver = sdl.Color{R: 192, G: 192, B: 192, A: 255}
Gray = sdl.Color{R: 128, G: 128, B: 128, A: 255}
Black = sdl.Color{R: 0, G: 0, B: 0, A: 255}
Red = sdl.Color{R: 255, G: 0, B: 0, A: 255}
Maroon = sdl.Color{R: 128, G: 0, B: 0, A: 255}
Yellow = sdl.Color{R: 255, G: 255, B: 0, A: 255}
Olive = sdl.Color{R: 128, G: 128, B: 0, A: 255}
Lime = sdl.Color{R: 0, G: 255, B: 0, A: 255}
Green = sdl.Color{R: 0, G: 128, B: 0, A: 255}
Aqua = sdl.Color{R: 0, G: 255, B: 255, A: 255}
Teal = sdl.Color{R: 0, G: 128, B: 128, A: 255}
Blue = sdl.Color{R: 0, G: 0, B: 255, A: 255}
Navy = sdl.Color{R: 0, G: 0, B: 128, A: 255}
Fuchsia = sdl.Color{R: 255, G: 0, B: 255, A: 255}
Purple = sdl.Color{R: 128, G: 0, B: 128, A: 255}
)
// Grayscale
var (
Gainsboro = sdl.Color{R: 220, G: 220, B: 220, A: 255}
LightGray = sdl.Color{R: 211, G: 211, B: 211, A: 255}
DarkGray = sdl.Color{R: 169, G: 169, B: 169, A: 255}
DimGray = sdl.Color{R: 105, G: 105, B: 105, A: 255}
LightSlateGray = sdl.Color{R: 119, G: 136, B: 153, A: 255}
SlateGray = sdl.Color{R: 112, G: 128, B: 144, A: 255}
DarkSlateGray = sdl.Color{R: 47, G: 79, B: 79, A: 255}
)
// Error Colors
var (
ErrorRed = sdl.Color{R: 255, G: 80, B: 80, A: 255}
)
// Pinks & Reds
var (
Pink = sdl.Color{R: 255, G: 192, B: 203, A: 255}
LightPink = sdl.Color{R: 255, G: 182, B: 193, A: 255}
HotPink = sdl.Color{R: 255, G: 105, B: 180, A: 255}
DeepPink = sdl.Color{R: 255, G: 20, B: 147, A: 255}
PaleVioletRed = sdl.Color{R: 219, G: 112, B: 147, A: 255}
MediumVioletRed = sdl.Color{R: 199, G: 21, B: 133, A: 255}
LightSalmon = sdl.Color{R: 255, G: 160, B: 122, A: 255}
Salmon = sdl.Color{R: 250, G: 128, B: 114, A: 255}
DarkSalmon = sdl.Color{R: 233, G: 150, B: 122, A: 255}
LightCoral = sdl.Color{R: 240, G: 128, B: 128, A: 255}
IndianRed = sdl.Color{R: 205, G: 92, B: 92, A: 255}
Crimson = sdl.Color{R: 220, G: 20, B: 60, A: 255}
Firebrick = sdl.Color{R: 178, G: 34, B: 34, A: 255}
DarkRed = sdl.Color{R: 139, G: 0, B: 0, A: 255}
)
// Oranges
var (
OrangeRed = sdl.Color{R: 255, G: 69, B: 0, A: 255}
Tomato = sdl.Color{R: 255, G: 99, B: 71, A: 255}
Coral = sdl.Color{R: 255, G: 127, B: 80, A: 255}
DarkOrange = sdl.Color{R: 255, G: 140, B: 0, A: 255}
Orange = sdl.Color{R: 255, G: 165, B: 0, A: 255}
)
// Yellows
var (
Gold = sdl.Color{R: 255, G: 215, B: 0, A: 255}
LightYellow = sdl.Color{R: 255, G: 255, B: 224, A: 255}
LemonChiffon = sdl.Color{R: 255, G: 250, B: 205, A: 255}
LightGoldenrodYellow = sdl.Color{R: 250, G: 250, B: 210, A: 255}
PapayaWhip = sdl.Color{R: 255, G: 239, B: 213, A: 255}
Moccasin = sdl.Color{R: 255, G: 228, B: 181, A: 255}
PeachPuff = sdl.Color{R: 255, G: 218, B: 185, A: 255}
PaleGoldenrod = sdl.Color{R: 238, G: 232, B: 170, A: 255}
Khaki = sdl.Color{R: 240, G: 230, B: 140, A: 255}
DarkKhaki = sdl.Color{R: 189, G: 183, B: 107, A: 255}
)
// Purples & Violets
var (
Lavender = sdl.Color{R: 230, G: 230, B: 250, A: 255}
Thistle = sdl.Color{R: 216, G: 191, B: 216, A: 255}
Plum = sdl.Color{R: 221, G: 160, B: 221, A: 255}
Violet = sdl.Color{R: 238, G: 130, B: 238, A: 255}
Orchid = sdl.Color{R: 218, G: 112, B: 214, A: 255}
Magenta = sdl.Color{R: 255, G: 0, B: 255, A: 255}
MediumOrchid = sdl.Color{R: 186, G: 85, B: 211, A: 255}
MediumPurple = sdl.Color{R: 147, G: 112, B: 219, A: 255}
RebeccaPurple = sdl.Color{R: 102, G: 51, B: 153, A: 255}
BlueViolet = sdl.Color{R: 138, G: 43, B: 226, A: 255}
DarkViolet = sdl.Color{R: 148, G: 0, B: 211, A: 255}
DarkOrchid = sdl.Color{R: 153, G: 50, B: 204, A: 255}
DarkMagenta = sdl.Color{R: 139, G: 0, B: 139, A: 255}
Indigo = sdl.Color{R: 75, G: 0, B: 130, A: 255}
SlateBlue = sdl.Color{R: 106, G: 90, B: 205, A: 255}
DarkSlateBlue = sdl.Color{R: 72, G: 61, B: 139, A: 255}
)
// Greens
var (
GreenYellow = sdl.Color{R: 173, G: 255, B: 47, A: 255}
Chartreuse = sdl.Color{R: 127, G: 255, B: 0, A: 255}
LawnGreen = sdl.Color{R: 124, G: 252, B: 0, A: 255}
LimeGreen = sdl.Color{R: 50, G: 205, B: 50, A: 255}
PaleGreen = sdl.Color{R: 152, G: 251, B: 152, A: 255}
LightGreen = sdl.Color{R: 144, G: 238, B: 144, A: 255}
MediumSpringGreen = sdl.Color{R: 0, G: 250, B: 154, A: 255}
SpringGreen = sdl.Color{R: 0, G: 255, B: 127, A: 255}
MediumSeaGreen = sdl.Color{R: 60, G: 179, B: 113, A: 255}
SeaGreen = sdl.Color{R: 46, G: 139, B: 87, A: 255}
ForestGreen = sdl.Color{R: 34, G: 139, B: 34, A: 255}
DarkGreen = sdl.Color{R: 0, G: 100, B: 0, A: 255}
YellowGreen = sdl.Color{R: 154, G: 205, B: 50, A: 255}
OliveDrab = sdl.Color{R: 107, G: 142, B: 35, A: 255}
DarkOliveGreen = sdl.Color{R: 85, G: 107, B: 47, A: 255}
MediumAquamarine = sdl.Color{R: 102, G: 205, B: 170, A: 255}
DarkSeaGreen = sdl.Color{R: 143, G: 188, B: 143, A: 255}
LightSeaGreen = sdl.Color{R: 32, G: 178, B: 170, A: 255}
DarkCyan = sdl.Color{R: 0, G: 139, B: 139, A: 255}
)
// Blues & Cyans
var (
LightCyan = sdl.Color{R: 224, G: 255, B: 255, A: 255}
Cyan = sdl.Color{R: 0, G: 255, B: 255, A: 255}
AquaMarine = sdl.Color{R: 127, G: 255, B: 212, A: 255}
PaleTurquoise = sdl.Color{R: 175, G: 238, B: 238, A: 255}
Turquoise = sdl.Color{R: 64, G: 224, B: 208, A: 255}
MediumTurquoise = sdl.Color{R: 72, G: 209, B: 204, A: 255}
DarkTurquoise = sdl.Color{R: 0, G: 206, B: 209, A: 255}
CadetBlue = sdl.Color{R: 95, G: 158, B: 160, A: 255}
SteelBlue = sdl.Color{R: 70, G: 130, B: 180, A: 255}
LightSteelBlue = sdl.Color{R: 176, G: 196, B: 222, A: 255}
PowderBlue = sdl.Color{R: 176, G: 224, B: 230, A: 255}
LightBlue = sdl.Color{R: 173, G: 216, B: 230, A: 255}
SkyBlue = sdl.Color{R: 135, G: 206, B: 235, A: 255}
LightSkyBlue = sdl.Color{R: 135, G: 206, B: 250, A: 255}
DeepSkyBlue = sdl.Color{R: 0, G: 191, B: 255, A: 255}
DodgerBlue = sdl.Color{R: 30, G: 144, B: 255, A: 255}
CornflowerBlue = sdl.Color{R: 100, G: 149, B: 237, A: 255}
MediumSlateBlue = sdl.Color{R: 123, G: 104, B: 238, A: 255}
RoyalBlue = sdl.Color{R: 65, G: 105, B: 225, A: 255}
MediumBlue = sdl.Color{R: 0, G: 0, B: 205, A: 255}
DarkBlue = sdl.Color{R: 0, G: 0, B: 139, A: 255}
MidnightBlue = sdl.Color{R: 25, G: 25, B: 112, A: 255}
)
// Browns
var (
Cornsilk = sdl.Color{R: 255, G: 248, B: 220, A: 255}
BlanchedAlmond = sdl.Color{R: 255, G: 235, B: 205, A: 255}
Bisque = sdl.Color{R: 255, G: 228, B: 196, A: 255}
NavajoWhite = sdl.Color{R: 255, G: 222, B: 173, A: 255}
Wheat = sdl.Color{R: 245, G: 222, B: 179, A: 255}
BurlyWood = sdl.Color{R: 222, G: 184, B: 135, A: 255}
Tan = sdl.Color{R: 210, G: 180, B: 140, A: 255}
RosyBrown = sdl.Color{R: 188, G: 143, B: 143, A: 255}
SandyBrown = sdl.Color{R: 244, G: 164, B: 96, A: 255}
Goldenrod = sdl.Color{R: 218, G: 165, B: 32, A: 255}
DarkGoldenrod = sdl.Color{R: 184, G: 134, B: 11, A: 255}
Peru = sdl.Color{R: 205, G: 133, B: 63, A: 255}
Chocolate = sdl.Color{R: 210, G: 105, B: 30, A: 255}
SaddleBrown = sdl.Color{R: 139, G: 69, B: 19, A: 255}
Sienna = sdl.Color{R: 160, G: 82, B: 45, A: 255}
Brown = sdl.Color{R: 165, G: 42, B: 42, A: 255}
)
// Whites
var (
Snow = sdl.Color{R: 255, G: 250, B: 250, A: 255}
Honeydew = sdl.Color{R: 240, G: 255, B: 240, A: 255}
MintCream = sdl.Color{R: 245, G: 255, B: 250, A: 255}
Azure = sdl.Color{R: 240, G: 255, B: 255, A: 255}
AliceBlue = sdl.Color{R: 240, G: 248, B: 255, A: 255}
GhostWhite = sdl.Color{R: 248, G: 248, B: 255, A: 255}
WhiteSmoke = sdl.Color{R: 245, G: 245, B: 245, A: 255}
Seashell = sdl.Color{R: 255, G: 245, B: 238, A: 255}
Beige = sdl.Color{R: 245, G: 245, B: 220, A: 255}
OldLace = sdl.Color{R: 253, G: 245, B: 230, A: 255}
FloralWhite = sdl.Color{R: 255, G: 250, B: 240, A: 255}
Ivory = sdl.Color{R: 255, G: 255, B: 240, A: 255}
AntiqueWhite = sdl.Color{R: 250, G: 235, B: 215, A: 255}
Linen = sdl.Color{R: 250, G: 240, B: 230, A: 255}
LavenderBlush = sdl.Color{R: 255, G: 240, B: 245, A: 255}
MistyRose = sdl.Color{R: 255, G: 228, B: 225, A: 255}
)
+488
View File
@@ -0,0 +1,488 @@
package db
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"rhemawavetech.com/rhemawave/internal/logger"
)
var DB *gorm.DB
// configCache is an in-memory cache of config values to avoid repeated DB queries.
var configCache = struct {
sync.RWMutex
m map[string]string
}{m: make(map[string]string)}
type Config struct {
gorm.Model
Key string `gorm:"uniqueIndex"`
Value string
}
type Proclamations struct {
gorm.Model
Title string
Content string
Commercial bool
Selected bool
Favorite bool
Available bool
}
type Log struct {
gorm.Model
Timestamp string
Level string
Message string
}
func (Log) TableName() string {
return "log_entries"
}
type TOSSection struct {
gorm.Model
SectionNumber int `gorm:"uniqueIndex"`
Title string
Content string // raw HTML
}
var defaultConfigValues = map[string]string{
"home_dir": "",
"assets_dir": "", // Set dynamically in seedDefaults() to $HOME/.local/share/rhemawave
"log_dir": "", // Set dynamically in seedDefaults() to $HOME/.local/share/rhemawave/logs.log
"email_address": "",
"license_key": "",
"system_type": "home",
"system_capabilities": "",
"selected_audio": "audio/audio.wav",
"audio_device_name": "",
"application_font": "Roboto-Regular",
"init_font_size": "128",
"standard_font_size": "32",
"line_pattern": "fibonacci",
"last_volume": "128",
"audio_enabled": "true",
"tos_section_1_accepted": "false",
"tos_section_2_accepted": "false",
"tos_section_3_accepted": "false",
"tos_section_4_accepted": "false",
"tos_section_5_accepted": "false",
"tos_section_6_accepted": "false",
"tos_section_7_accepted": "false",
"tos_section_8_accepted": "false",
"tos_section_9_accepted": "false",
"tos_section_10_accepted": "false",
}
// InitDatabase initializes the SQLite database connection
// and performs auto-migration for all models
func InitDatabase(dbpath string) error {
// Create the directory for the database if it doesn't exist
dbDir := filepath.Dir(dbpath)
if err := os.MkdirAll(dbDir, 0700); err != nil {
log.Printf("Error creating database directory %s: %v", dbDir, err)
return err
}
// Check if database file exists
_, err := os.Stat(dbpath)
dbExists := !os.IsNotExist(err)
if !dbExists {
log.Printf("Database file does not exist at %s, creating new database", dbpath)
}
dsn := fmt.Sprintf("file:%s", dbpath)
DB, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
if err != nil {
log.Printf("Error opening database: %v", err)
return err
}
// Get underlying sql.DB to set pragmas
sqlDB, err := DB.DB()
if err != nil {
logger.Error("Error getting underlying database connection: %v", err)
return err
}
// Configure SQLite for reliability and performance
pragmas := []string{
"PRAGMA journal_mode=WAL", // Write-Ahead Logging for better concurrency
"PRAGMA synchronous=FULL", // Maximum durability
"PRAGMA foreign_keys=ON", // Enable foreign key constraints
"PRAGMA busy_timeout=5000", // Wait up to 5 seconds for locks
"PRAGMA temp_store=MEMORY", // Keep temp tables in memory
}
for _, pragma := range pragmas {
if _, err := sqlDB.Exec(pragma); err != nil {
logger.Error("Error setting pragma %s: %v", pragma, err)
return fmt.Errorf("failed to set pragma: %w", err)
}
}
if err := sqlDB.Ping(); err != nil {
logger.Error("Error pinging database: %v", err)
return fmt.Errorf("database connection failed: %w", err)
}
log.Println("Database connected successfully")
// Auto-migrate models
err = DB.AutoMigrate(&Config{}, &Proclamations{}, &Log{}, &TOSSection{})
if err != nil {
logger.Error("Error migrating database: %v", err)
return err
}
log.Println("Database migrated successfully")
// Seed defaults
err = ensureDefaultsExist()
if err != nil {
logger.Error("Error seeding default configuration values: %v", err)
return err
}
err = seedDefaultProclamations()
if err != nil {
logger.Error("Error seeding default proclamations: %v", err)
return err
}
err = seedDefaultTOSSections()
if err != nil {
logger.Error("Error seeding default TOS sections: %v", err)
return err
}
log.Println("Database initialized successfully")
return nil
}
// seedDefaultProclamations reads proclamation text files from the assets directory
// and populates the database on first run
func seedDefaultProclamations() error {
var count int64
err := DB.Model(&Proclamations{}).Count(&count).Error
if err != nil {
logger.Error("Error counting proclamations: %v", err)
return err
}
// Only seed if table is empty
if count > 0 {
return nil
}
// Get the assets directory from config
assetsDir, err := GetConfigValue("assets_dir")
if err != nil {
logger.Error("Error getting assets_dir: %v", err)
return err
}
proclamationsBaseDir := assetsDir + "/proclamations"
// Process both home and commercial directories
subdirs := []string{"home", "commercial"}
totalSeeded := 0
for _, subdir := range subdirs {
proclamationsDir := proclamationsBaseDir + "/" + subdir
isCommercial := (subdir == "commercial")
// Read all .txt files from the subdirectory
files, err := os.ReadDir(proclamationsDir)
if err != nil {
logger.Error("Error reading proclamations directory %s: %v", proclamationsDir, err)
continue // Continue with other directories even if one fails
}
for _, file := range files {
if file.IsDir() || !strings.HasSuffix(file.Name(), ".txt") {
continue
}
filePath := proclamationsDir + "/" + file.Name()
content, err := os.ReadFile(filePath)
if err != nil {
logger.Error("Error reading file %s: %v", filePath, err)
continue
}
// Extract title from filename (remove .txt extension)
title := strings.TrimSuffix(file.Name(), ".txt")
// Select "Relaxation" as the default proclamation
isSelected := (title == "Relaxation")
proclamation := Proclamations{
Title: title,
Content: string(content),
Commercial: isCommercial,
Selected: isSelected,
Favorite: false,
Available: true,
}
if err := DB.Create(&proclamation).Error; err != nil {
logger.Error("Error creating proclamation from %s: %v", file.Name(), err)
return err
}
if isSelected {
logger.Error("Set default proclamation: %s", title)
}
totalSeeded++
}
}
logger.Error("Seeded %d default proclamations from %s", totalSeeded, proclamationsBaseDir)
return nil
}
// GetConfigValue retrieves a configuration value by key.
// Results are cached in memory to avoid repeated DB queries.
func GetConfigValue(key string) (string, error) {
configCache.RLock()
if v, ok := configCache.m[key]; ok {
configCache.RUnlock()
return v, nil
}
configCache.RUnlock()
var config Config
result := DB.First(&config, "key = ?", key)
if result.Error != nil {
return "", result.Error
}
configCache.Lock()
configCache.m[key] = config.Value
configCache.Unlock()
return config.Value, nil
}
// GetConfigValues retrieves all configuration key-value pairs
// Returns a map of key-value pairs OR any error encountered
// This is mainly for compatibility with legacy code, but can
// be useful for bulkl config referrals
func GetConfigValues() (map[string]string, error) {
var configs []Config
result := DB.Find(&configs)
if result.Error != nil {
return nil, result.Error
}
configMap := make(map[string]string)
for _, config := range configs {
configMap[config.Key] = config.Value
}
return configMap, nil
}
// SetConfigValue sets a configuration value by key
// Returns any error encountered || nil
func SetConfigValue(key, value string) error {
// First try to find existing config
var config Config
result := DB.Where("key = ?", key).First(&config)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
// Record doesn't exist, create new one
config = Config{Key: key, Value: value}
if err := DB.Create(&config).Error; err != nil {
return fmt.Errorf("failed to create config value for key %s: %w", key, err)
}
} else {
// Some other error occurred
return fmt.Errorf("failed to query config for key %s: %w", key, result.Error)
}
} else {
// Record exists, update it
config.Value = value
if err := DB.Save(&config).Error; err != nil {
return fmt.Errorf("failed to update config value for key %s: %w", key, err)
}
}
// Update cache
configCache.Lock()
configCache.m[key] = value
configCache.Unlock()
return nil
}
// GetProclamations retrieves all proclamation entries from the database
// Returns a slice of Proclamations OR any error encountered
func GetProclamations() ([]Proclamations, error) {
var proclamations []Proclamations
result := DB.Find(&proclamations)
if result.Error != nil {
return nil, result.Error
}
return proclamations, nil
}
// seedDefaultTOSSections reads HTML files from the assets directory and populates the
// TOS sections table on first run.
func seedDefaultTOSSections() error {
var count int64
if err := DB.Model(&TOSSection{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
assetsDir, err := GetConfigValue("assets_dir")
if err != nil {
return fmt.Errorf("getting assets_dir: %w", err)
}
tosDir := filepath.Join(assetsDir, "tos")
files, err := os.ReadDir(tosDir)
if err != nil {
return fmt.Errorf("reading tos directory %s: %w", tosDir, err)
}
seeded := 0
for _, file := range files {
if file.IsDir() || !strings.HasSuffix(file.Name(), ".html") {
continue
}
// Parse filename: "01 - WELCOME.html" -> number=1, title="WELCOME"
name := strings.TrimSuffix(file.Name(), ".html")
parts := strings.SplitN(name, " - ", 2)
if len(parts) != 2 {
logger.Error("Skipping TOS file with unexpected name format: %s", file.Name())
continue
}
var sectionNum int
if _, err := fmt.Sscanf(parts[0], "%d", &sectionNum); err != nil {
logger.Error("Skipping TOS file, cannot parse section number: %s", file.Name())
continue
}
title := strings.TrimSpace(parts[1])
content, err := os.ReadFile(filepath.Join(tosDir, file.Name()))
if err != nil {
logger.Error("Error reading TOS file %s: %v", file.Name(), err)
continue
}
section := TOSSection{
SectionNumber: sectionNum,
Title: title,
Content: string(content),
}
if err := DB.Create(&section).Error; err != nil {
return fmt.Errorf("creating TOS section from %s: %w", file.Name(), err)
}
seeded++
}
logger.Info("Seeded %d TOS sections from %s", seeded, tosDir)
return nil
}
// GetTOSSections returns all TOS sections ordered by section number.
func GetTOSSections() ([]TOSSection, error) {
var sections []TOSSection
result := DB.Order("section_number asc").Find(&sections)
if result.Error != nil {
return nil, result.Error
}
return sections, nil
}
// GetLogs retrieves log entries from the database, ordered by creation time ascending.
// If limit > 0, at most that many entries are returned.
func GetLogs(limit int) ([]Log, error) {
var logs []Log
query := DB.Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
return logs, query.Find(&logs).Error
}
// ensureDefaultsExist checks the current configs table rows and inserts any new rows
// from defaultConfigValues to the database if they don't exist.
// This is essentially an automatic migration for new config values.
func ensureDefaultsExist() error {
for key, value := range defaultConfigValues {
var config Config
result := DB.Where("key = ?", key).First(&config)
// Always re-resolve path-based config values from the current binary location
switch key {
case "assets_dir":
value = resolveAssetsDir()
case "log_dir":
value = filepath.Join(resolveAssetsDir(), "logs.log")
}
if result.Error == gorm.ErrRecordNotFound {
// Key doesn't exist, create it with default value
if err := DB.Create(&Config{Key: key, Value: value}).Error; err != nil {
return fmt.Errorf("failed to create default config for %s: %w", key, err)
}
log.Printf("Added missing config key: %s", key)
} else if result.Error != nil {
return result.Error
} else if (key == "assets_dir" || key == "log_dir") && config.Value != value {
// Update stale path-based values (e.g. after Nix store update)
config.Value = value
if err := DB.Save(&config).Error; err != nil {
return fmt.Errorf("failed to update config for %s: %w", key, err)
}
log.Printf("Updated stale config key: %s", key)
} else if key == "selected_audio" && config.Value == "audio/7.83.wav" {
// Migrate renamed audio file
config.Value = value
if err := DB.Save(&config).Error; err != nil {
return fmt.Errorf("failed to migrate config for %s: %w", key, err)
}
log.Printf("Migrated config key: %s", key)
}
}
return nil
}
func AudioEnabled() bool {
on, err := GetConfigValue("audio_enabled")
if err != nil {
return false
}
return on == "true"
}
// resolveAssetsDir returns the assets directory by resolving relative to the
// running binary. For a binary at .../bin/rhemawave, assets are at
// .../share/rhemawave. Falls back to /usr/local/share/rhemawave.
func resolveAssetsDir() string {
exe, err := os.Executable()
if err == nil {
exe, err = filepath.EvalSymlinks(exe)
}
if err == nil {
return filepath.Join(filepath.Dir(exe), "..", "share", "rhemawave")
}
return filepath.Join("/usr", "local", "share", "rhemawave")
}
+829
View File
@@ -0,0 +1,829 @@
package fontManager
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/graphics"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
// FontManager handles the loading and management of fonts.
type FontManager struct {
Fonts map[string]*FontEntry
// Reusable GPU objects for string texture rendering
stringFBO uint32
stringVAO uint32
stringVBO uint32
}
var (
fontManagerInstance *FontManager
fontManagerOnce sync.Once
initErr error // Package-level variable to capture error from within sync.Once
)
// InitFontManager initializes the FontManager singleton.
// It must be called once at startup after ShaderManager has been initialized.
func InitFontManager() error {
fontManagerOnce.Do(func() {
// Dependency check: Ensure ShaderManager is ready
sm := shaderManager.Get()
if sm == nil {
initErr = fmt.Errorf("cannot initialize FontManager because ShaderManager is not initialized")
return // Exit the anonymous function
}
fm := &FontManager{
Fonts: make(map[string]*FontEntry),
}
assetsDir, err := db.GetConfigValue("assets_dir")
if err != nil {
initErr = fmt.Errorf("could not get assets_dir from config: %w", err)
return
}
fontPath := filepath.Join(assetsDir, "fonts")
if err := fm.loadFonts(fontPath); err != nil {
initErr = fmt.Errorf("could not load fonts from '%s': %w", fontPath, err)
return
}
// --- NEW: Create the scrambled font after loading normal fonts ---
// We will use "DejaVuSans" as the base. If you use a different default font,
// change this string.
baseFontForScramble := "Roboto-Regular"
if _, ok := fm.Fonts[baseFontForScramble]; ok {
if err := fm.CreateScrambledFont(baseFontForScramble, "Scrambled"); err != nil {
// This is not a fatal error for the whole application, so we just log it.
}
} else {
}
// --- END NEW ---
// Create reusable GPU objects for string texture rendering
gl.GenFramebuffers(1, &fm.stringFBO)
gl.GenVertexArrays(1, &fm.stringVAO)
gl.GenBuffers(1, &fm.stringVBO)
// Set up VAO attribute layout once
gl.BindVertexArray(fm.stringVAO)
gl.BindBuffer(gl.ARRAY_BUFFER, fm.stringVBO)
gl.BufferData(gl.ARRAY_BUFFER, 6*4*4, nil, gl.DYNAMIC_DRAW) // 6 vertices * 4 floats * 4 bytes
gl.EnableVertexAttribArray(0)
gl.VertexAttribPointerWithOffset(0, 2, gl.FLOAT, false, 4*4, 0)
gl.EnableVertexAttribArray(1)
gl.VertexAttribPointerWithOffset(1, 2, gl.FLOAT, false, 4*4, 2*4)
gl.BindVertexArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
fontManagerInstance = fm
})
// Return the error that might have been captured inside the sync.Once block.
return initErr
}
// Get returns the singleton instance of the FontManager.
// It will panic if InitFontManager has not been called successfully first.
func Get() *FontManager {
if fontManagerInstance == nil {
panic("FontManager has not been initialized or initialization failed. Call InitFontManager at application startup.")
}
return fontManagerInstance
}
// FontEntry combines related Font information together
type FontEntry struct {
Name string
Font *ttf.Font
Map *FontMap
}
// FontMap contains the character map for a specific font
type FontMap struct {
FontMapTextureID uint32 // Memory address of the FontMapTexture within the GPU
Width int32 // The width of the font map texture
Height int32 // The height of the font map FontMap Texture
Glyphs map[rune]Glyph // A map of runes and their locations within the Fontmap
padding int32
}
// TempGlyph is a temporary struct used only during font map creation.
type TempGlyph struct {
Rune rune
Advance int
surface *sdl.Surface
}
// Glyph contains the rendering information for a single character
type Glyph struct {
Rune rune // The character this glyph represents.
SrcRect sdl.Rect // The source rectangle in the texture atlas.
Advance int // How far to advance the cursor after drawing this glyph.
TextureID uint32 // The ID of the texture atlas this glyph belongs to.
}
// loadFonts scans the fonts directory, loads all .ttf/.otf files, and creates a font map for each.
func (fm *FontManager) loadFonts(fontDir string) error {
files, err := os.ReadDir(fontDir)
if err != nil {
return fmt.Errorf("could not read font directory '%s': %w", fontDir, err)
}
for _, file := range files {
if file.IsDir() {
continue
}
fileName := file.Name()
if strings.HasSuffix(strings.ToLower(fileName), ".ttf") || strings.HasSuffix(strings.ToLower(fileName), ".otf") {
fontPath := filepath.Join(fontDir, fileName)
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("could not get standard_font_size from config: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("could not convert init_font_size to int: %w", err)
}
font, err := ttf.OpenFont(fontPath, baseFontSize) // Using a higher resolution for better quality
if err != nil {
continue
}
fontName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
fm.Fonts[fontName] = &FontEntry{
Name: fontName,
Font: font,
Map: nil,
}
}
}
for _, fontEntry := range fm.Fonts {
if err := fm.createFontMap(fontEntry, false, nil); err != nil {
return fmt.Errorf("unable to create Font Map for font: %s : %v", fontEntry.Name, err)
}
}
return nil
}
// CreateScrambledFont generates a new font entry by scrambling the glyphs of a base font.
func (fm *FontManager) CreateScrambledFont(baseFontName string, newFontName string) error {
baseFont, ok := fm.Fonts[baseFontName]
if !ok {
return fmt.Errorf("base font '%s' not found", baseFontName)
}
scrambledFontEntry := &FontEntry{
Name: newFontName,
Font: baseFont.Font, // It uses the same underlying TTF font object for rendering
Map: nil,
}
if err := fm.createFontMap(scrambledFontEntry, true, baseFont); err != nil {
return fmt.Errorf("unable to create scrambled font map: %w", err)
}
fm.Fonts[newFontName] = scrambledFontEntry
return nil
}
// ATTENTION: TED
//
// # Deterministic Fibonacci-Based Visual Pattern Generation for Character Data
//
// This method transforms rendered text characters into structured visual patterns that produce
// character-specific spatial frequency signatures while maintaining perfect reversibility.
//
// The technology addresses a fundamental limitation in subliminal text display: conventional
// approaches either use unmodified text (consciously readable, defeating the subliminal purpose)
// or simple blurring/masking (destroys information content and character-specific structure).
// Our method preserves complete character information in a transformed visual state that remains
// illegible to conscious observation while generating structured spatial patterns.
//
// The approach treats each rendered character as a two-dimensional grid and applies deterministic
// geometric transformations parameterized by Fibonacci sequence values. The character's Unicode
// value seeds all transformation parameters, ensuring identical characters produce identical
// patterns while different characters generate distinct spatial signatures. Multiple transformation
// stages compound to create high-entropy visual output with underlying mathematical structure.
//
// Key differentiators from existing solutions:
// - Operates on rendered visual data rather than underlying text data or encrypted values
// - Generates character-specific spatial frequency patterns rather than uniform noise
// - Uses Fibonacci-sequence-derived parameters to create mathematically structured transformations
// - Maintains pixel-perfect reversibility despite high visual entropy
// - Produces deterministic, reproducible patterns for each character
//
// Within our Rhema Wave generation system, these transformed characters create spatial interference
// patterns designed for subconscious processing. Each character contributes a unique frequency
// signature to the overall visual field, with Fibonacci-based geometry intended to create harmonic
// relationships between character patterns. The result is a consciously illegible display that
// maintains structured information content in the spatial domain, forming a component of the
// broader Rhema Wave output.
func (fm *FontManager) scrambleSurface(surface *sdl.Surface, r rune) error {
if surface.W < 8 || surface.H < 8 {
return nil
}
if err := surface.Lock(); err != nil {
return fmt.Errorf("could not lock surface for scrambling: %w", err)
}
defer surface.Unlock()
W := int(surface.W)
H := int(surface.H)
// Use much finer grid for complete illegibility
// Fibonacci number determines grid size: 5, 8, or 13
fib := []int{5, 8, 13}
fibIndex := int(r) % len(fib)
gridSize := fib[fibIndex]
// Ensure we have at least 8x8 grid for strong scrambling
if gridSize < 8 {
gridSize = 8
}
blockW := W / gridSize
blockH := H / gridSize
if blockW == 0 || blockH == 0 {
// Surface too small, fall back to 4x4
gridSize = 4
blockW = W / gridSize
blockH = H / gridSize
if blockW == 0 || blockH == 0 {
return nil
}
}
numBlocks := gridSize * gridSize
// Create complete permutation using Fibonacci linear congruential generator
blockIndices := make([]int, numBlocks)
// Use Fibonacci numbers for the LCG parameters
// F(7)=13, F(8)=21, F(9)=34, F(10)=55, F(11)=89
fibSeq := []int{13, 21, 34, 55, 89, 144}
multiplier := fibSeq[int(r)%len(fibSeq)]
increment := fibSeq[(int(r)+1)%len(fibSeq)]
// Generate pseudo-random permutation
for i := 0; i < numBlocks; i++ {
blockIndices[i] = (multiplier*i + increment*int(r)) % numBlocks
}
// Additional scrambling: apply multiple Fibonacci-based transformations
numTransforms := 3 + (int(r) % 5) // 3-7 transformations
for t := 0; t < numTransforms; t++ {
transformType := (int(r) + t*fibSeq[t%len(fibSeq)]) % 5
switch transformType {
case 0: // Reverse sections
sectionSize := gridSize / 2
for i := 0; i < numBlocks; i += sectionSize {
end := i + sectionSize
if end > numBlocks {
end = numBlocks
}
reverseSlice(blockIndices[i:end])
}
case 1: // Spiral rotation
// Rotate blocks in a spiral pattern
for layer := 0; layer < gridSize/2; layer++ {
rotateLayer(blockIndices, gridSize, layer)
}
case 2: // Fibonacci walk swap
step := fib[t%len(fib)]
for i := 0; i < numBlocks-step; i += step {
blockIndices[i], blockIndices[i+step] = blockIndices[i+step], blockIndices[i]
}
case 3: // Checkerboard transpose
for i := 0; i < gridSize; i++ {
for j := 0; j < gridSize; j++ {
if (i+j)%2 == 0 {
idx1 := i*gridSize + j
idx2 := j*gridSize + i
if idx2 < numBlocks {
blockIndices[idx1], blockIndices[idx2] = blockIndices[idx2], blockIndices[idx1]
}
}
}
}
case 4: // Random-walk permutation
walkLen := fib[(t+1)%len(fib)]
pos := int(r+rune(t)) % numBlocks
for w := 0; w < walkLen && pos < numBlocks-1; w++ {
nextPos := (pos + fib[w%len(fib)]) % numBlocks
blockIndices[pos], blockIndices[nextPos] = blockIndices[nextPos], blockIndices[pos]
pos = nextPos
}
}
}
// Copy pixels to perform the scrambling
pixels := surface.Pixels()
bytesPerPixel := int(surface.Format.BytesPerPixel)
pitch := int(surface.Pitch)
originalPixels := make([]byte, len(pixels))
copy(originalPixels, pixels)
// Rearrange all blocks
for dstIdx := 0; dstIdx < numBlocks; dstIdx++ {
srcIdx := blockIndices[dstIdx]
srcCol := srcIdx % gridSize
srcRow := srcIdx / gridSize
dstCol := dstIdx % gridSize
dstRow := dstIdx / gridSize
srcX := srcCol * blockW
srcY := srcRow * blockH
dstX := dstCol * blockW
dstY := dstRow * blockH
// Copy block pixel by pixel
for y := 0; y < blockH; y++ {
if srcY+y >= H || dstY+y >= H {
continue
}
srcOffset := (srcY+y)*pitch + srcX*bytesPerPixel
dstOffset := (dstY+y)*pitch + dstX*bytesPerPixel
rowLen := blockW * bytesPerPixel
if srcX+blockW > W {
rowLen = (W - srcX) * bytesPerPixel
}
if dstX+blockW > W {
maxLen := (W - dstX) * bytesPerPixel
if maxLen < rowLen {
rowLen = maxLen
}
}
if srcOffset+rowLen <= len(originalPixels) && dstOffset+rowLen <= len(pixels) {
copy(pixels[dstOffset:dstOffset+rowLen], originalPixels[srcOffset:srcOffset+rowLen])
}
}
}
return nil
}
// Helper function to reverse a slice
func reverseSlice(arr []int) {
for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {
arr[i], arr[j] = arr[j], arr[i]
}
}
// Helper function to rotate a layer in spiral pattern
func rotateLayer(arr []int, gridSize, layer int) {
if layer >= gridSize/2 {
return
}
// Extract the layer elements
var temp []int
// Top edge
for i := layer; i < gridSize-layer; i++ {
temp = append(temp, arr[layer*gridSize+i])
}
// Right edge
for i := layer + 1; i < gridSize-layer; i++ {
temp = append(temp, arr[i*gridSize+(gridSize-layer-1)])
}
// Bottom edge
if gridSize-layer-1 > layer {
for i := gridSize - layer - 2; i >= layer; i-- {
temp = append(temp, arr[(gridSize-layer-1)*gridSize+i])
}
}
// Left edge
if gridSize-layer-1 > layer {
for i := gridSize - layer - 2; i > layer; i-- {
temp = append(temp, arr[i*gridSize+layer])
}
}
// Rotate the temp array by 1 position
if len(temp) > 0 {
last := temp[len(temp)-1]
copy(temp[1:], temp[0:len(temp)-1])
temp[0] = last
}
// Put back the rotated elements
idx := 0
// Top edge
for i := layer; i < gridSize-layer && idx < len(temp); i++ {
arr[layer*gridSize+i] = temp[idx]
idx++
}
// Right edge
for i := layer + 1; i < gridSize-layer && idx < len(temp); i++ {
arr[i*gridSize+(gridSize-layer-1)] = temp[idx]
idx++
}
// Bottom edge
if gridSize-layer-1 > layer {
for i := gridSize - layer - 2; i >= layer && idx < len(temp); i-- {
arr[(gridSize-layer-1)*gridSize+i] = temp[idx]
idx++
}
}
// Left edge
if gridSize-layer-1 > layer {
for i := gridSize - layer - 2; i > layer && idx < len(temp); i-- {
arr[i*gridSize+layer] = temp[idx]
idx++
}
}
}
// createFontMap generates a texture atlas for a given font entry.
func (fm *FontManager) createFontMap(font *FontEntry, scramble bool, baseFont *FontEntry) error {
var chars []rune
for i := rune(32); i <= 126; i++ { // Common ASCII characters
chars = append(chars, i)
}
tempGlyphs := make([]TempGlyph, 0, len(chars))
var totalWidth, maxHeight int32
padding := int32(2) // 2 pixels of padding between glyphs
// If scrambling, use the base font's metrics but our new entry's name
fontToRenderFrom := font
if scramble && baseFont != nil {
fontToRenderFrom = baseFont
}
for _, char := range chars {
tempGlyph, err := fm.createTempGlyph(fontToRenderFrom, char)
if err != nil {
continue
}
// --- NEW: Scramble the surface if requested ---
if scramble {
if err := fm.scrambleSurface(tempGlyph.surface, char); err != nil {
// We can continue, it will just use the unscrambled glyph
}
}
// --- END NEW ---
tempGlyphs = append(tempGlyphs, tempGlyph)
totalWidth += tempGlyph.surface.W + padding
if tempGlyph.surface.H > maxHeight {
maxHeight = tempGlyph.surface.H
}
}
atlasSurface, err := sdl.CreateRGBSurface(0, totalWidth, maxHeight, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000)
if err != nil {
return fmt.Errorf("could not create atlas surface: %w", err)
}
defer atlasSurface.Free()
fontMap := &FontMap{
Glyphs: make(map[rune]Glyph),
padding: padding,
}
var currentX int32 = 0
for _, tempGlyph := range tempGlyphs {
dstRect := sdl.Rect{X: currentX, Y: 0, W: tempGlyph.surface.W, H: tempGlyph.surface.H}
if err := tempGlyph.surface.Blit(nil, atlasSurface, &dstRect); err != nil {
tempGlyph.surface.Free()
continue
}
fontMap.Glyphs[tempGlyph.Rune] = Glyph{
Rune: tempGlyph.Rune,
Advance: tempGlyph.Advance,
SrcRect: dstRect,
}
currentX += tempGlyph.surface.W + padding
tempGlyph.surface.Free()
}
textureID, err := graphics.CreateTextureFromSurface(atlasSurface)
if err != nil {
return fmt.Errorf("could not create texture from atlas surface: %w", err)
}
fontMap.FontMapTextureID = textureID
fontMap.Width = atlasSurface.W
fontMap.Height = atlasSurface.H
for r, g := range fontMap.Glyphs {
g.TextureID = textureID
fontMap.Glyphs[r] = g
}
font.Map = fontMap
return nil
}
// createTempGlyph renders a single character to a temporary SDL surface.
func (fm *FontManager) createTempGlyph(font *FontEntry, char rune) (TempGlyph, error) {
runeSurface, err := font.Font.RenderGlyphBlended(char, sdl.Color{R: 255, G: 255, B: 255, A: 255})
if err != nil {
return TempGlyph{}, fmt.Errorf("unable to create glyph for char '%c': %w", char, err)
}
metrics, err := font.Font.GlyphMetrics(char)
if err != nil {
runeSurface.Free()
return TempGlyph{}, fmt.Errorf("unable to fetch glyph metrics for char '%c': %w", char, err)
}
return TempGlyph{
Rune: char,
Advance: metrics.Advance,
surface: runeSurface,
}, nil
}
// GetFont retrieves a loaded font by its name.
func (fm *FontManager) GetFont(name string) (*FontEntry, bool) {
fontEntry, ok := fm.Fonts[name]
return fontEntry, ok
}
// GetGlyph retrieves the rendering data for a specific character from a font's map.
func (fm *FontManager) GetGlyph(fontName string, r rune) (Glyph, bool) {
fontEntry, ok := fm.Fonts[fontName]
if !ok || fontEntry.Map == nil {
return Glyph{}, false
}
glyph, ok := fontEntry.Map.Glyphs[r]
return glyph, ok
}
// GetFontNames returns a slice of all loaded font names.
func (fm *FontManager) GetFontNames() []string {
names := make([]string, 0, len(fm.Fonts))
for name := range fm.Fonts {
names = append(names, name)
}
return names
}
// Close frees all loaded font resources, including the generated textures.
func (fm *FontManager) Close() {
for _, fontEntry := range fm.Fonts {
if fontEntry.Map != nil && fontEntry.Map.FontMapTextureID != 0 {
gl.DeleteTextures(1, &fontEntry.Map.FontMapTextureID)
}
if fontEntry.Font != nil {
fontEntry.Font.Close()
}
}
if fm.stringFBO != 0 {
gl.DeleteFramebuffers(1, &fm.stringFBO)
}
if fm.stringVAO != 0 {
gl.DeleteVertexArrays(1, &fm.stringVAO)
}
if fm.stringVBO != 0 {
gl.DeleteBuffers(1, &fm.stringVBO)
}
}
func (fm *FontManager) CreateStringTexture(fontName string, text string, shader *shaderManager.Shader) (textureID uint32, width int32, height int32, err error) {
shaderID := shader.ProgramID
if fontName == "" || text == "" {
return 0, 0, 0, fmt.Errorf("font name and text cannot be blank")
}
selectedFont, ok := fm.Fonts[fontName]
if !ok || selectedFont.Map == nil {
return 0, 0, 0, fmt.Errorf("font '%s' not found or its font map is not created", fontName)
}
var totalWidth, maxHeight int32
glyphsToRender := make([]Glyph, 0, len(text))
for _, r := range text {
g, ok := selectedFont.Map.Glyphs[r]
if !ok {
g = selectedFont.Map.Glyphs['?'] // Fallback
}
totalWidth += g.SrcRect.W + selectedFont.Map.padding
if g.SrcRect.H > maxHeight {
maxHeight = g.SrcRect.H
}
glyphsToRender = append(glyphsToRender, g)
}
if totalWidth <= 0 || maxHeight <= 0 {
return 0, 0, 0, fmt.Errorf("calculated texture dimensions are invalid: %dx%d", totalWidth, maxHeight)
}
var lastFBO int32
gl.GetIntegerv(gl.FRAMEBUFFER_BINDING, &lastFBO)
var lastViewport [4]int32
gl.GetIntegerv(gl.VIEWPORT, &lastViewport[0])
gl.BindFramebuffer(gl.FRAMEBUFFER, fm.stringFBO)
var newTextureID uint32
gl.GenTextures(1, &newTextureID)
gl.BindTexture(gl.TEXTURE_2D, newTextureID)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, totalWidth, maxHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newTextureID, 0)
if gl.CheckFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE {
gl.BindFramebuffer(gl.FRAMEBUFFER, uint32(lastFBO))
gl.DeleteTextures(1, &newTextureID)
return 0, 0, 0, fmt.Errorf("framebuffer is not complete")
}
gl.Viewport(0, 0, totalWidth, maxHeight)
gl.ClearColor(0.0, 0.0, 0.0, 0.0)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.UseProgram(shaderID)
projection := mgl32.Ortho(0, float32(totalWidth), 0, float32(maxHeight), -1, 1)
gl.UniformMatrix4fv(shader.LocMVPMatrix, 1, false, &projection[0])
if shader.LocTextColor != -1 {
gl.Uniform4f(shader.LocTextColor, 1.0, 1.0, 1.0, 1.0)
}
gl.ActiveTexture(gl.TEXTURE0)
gl.Uniform1i(shader.LocTexture, 0)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BindTexture(gl.TEXTURE_2D, selectedFont.Map.FontMapTextureID)
gl.BindVertexArray(fm.stringVAO)
gl.BindBuffer(gl.ARRAY_BUFFER, fm.stringVBO)
var currentX float32
atlasW, atlasH := float32(selectedFont.Map.Width), float32(selectedFont.Map.Height)
for _, glyph := range glyphsToRender {
xPos := currentX
yPos := float32(0)
w, h := float32(glyph.SrcRect.W), float32(glyph.SrcRect.H)
u0 := float32(glyph.SrcRect.X) / atlasW
v0 := float32(glyph.SrcRect.Y) / atlasH
u1 := u0 + w/atlasW
v1 := v0 + h/atlasH
vertices := []float32{
xPos, yPos + h, u0, v0,
xPos, yPos, u0, v1,
xPos + w, yPos, u1, v1,
xPos, yPos + h, u0, v0,
xPos + w, yPos, u1, v1,
xPos + w, yPos + h, u1, v0,
}
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(vertices)*4, gl.Ptr(vertices))
gl.DrawArrays(gl.TRIANGLES, 0, 6)
currentX += float32(glyph.Advance)
}
gl.BindVertexArray(0)
gl.BindFramebuffer(gl.FRAMEBUFFER, uint32(lastFBO))
gl.Viewport(lastViewport[0], lastViewport[1], lastViewport[2], lastViewport[3])
gl.Disable(gl.BLEND)
return newTextureID, totalWidth, maxHeight, nil
}
// NewCreateStringTexture does the same thing as CreateStringTexture, except that it returns the width in pixels used by the glyphs
// in the texture, rather than the width of the texture itself. This can be used for trimming.
func (fm *FontManager) NewCreateStringTexture(fontName string, text string, shader *shaderManager.Shader) (textureID uint32, width float32, height int32, err error) {
shaderID := shader.ProgramID
if fontName == "" || text == "" {
return 0, 0, 0, fmt.Errorf("font name and text cannot be blank")
}
selectedFont, ok := fm.Fonts[fontName]
if !ok || selectedFont.Map == nil {
return 0, 0, 0, fmt.Errorf("font '%s' not found or its font map is not created", fontName)
}
var totalWidth, maxHeight int32
glyphsToRender := make([]Glyph, 0, len(text))
for _, r := range text {
g, ok := selectedFont.Map.Glyphs[r]
if !ok {
g = selectedFont.Map.Glyphs['?'] // Fallback
}
totalWidth += g.SrcRect.W + selectedFont.Map.padding
if g.SrcRect.H > maxHeight {
maxHeight = g.SrcRect.H
}
glyphsToRender = append(glyphsToRender, g)
}
if totalWidth <= 0 || maxHeight <= 0 {
return 0, 0, 0, fmt.Errorf("calculated texture dimensions are invalid: %dx%d", totalWidth, maxHeight)
}
var lastFBO int32
gl.GetIntegerv(gl.FRAMEBUFFER_BINDING, &lastFBO)
var lastViewport [4]int32
gl.GetIntegerv(gl.VIEWPORT, &lastViewport[0])
gl.BindFramebuffer(gl.FRAMEBUFFER, fm.stringFBO)
var newTextureID uint32
gl.GenTextures(1, &newTextureID)
gl.BindTexture(gl.TEXTURE_2D, newTextureID)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, totalWidth, maxHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newTextureID, 0)
if gl.CheckFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE {
gl.BindFramebuffer(gl.FRAMEBUFFER, uint32(lastFBO))
gl.DeleteTextures(1, &newTextureID)
return 0, 0, 0, fmt.Errorf("framebuffer is not complete")
}
gl.Viewport(0, 0, totalWidth, maxHeight)
gl.ClearColor(0.0, 0.0, 0.0, 0.0)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.UseProgram(shaderID)
projection := mgl32.Ortho(0, float32(totalWidth), 0, float32(maxHeight), -1, 1)
gl.UniformMatrix4fv(shader.LocMVPMatrix, 1, false, &projection[0])
if shader.LocTextColor != -1 {
gl.Uniform4f(shader.LocTextColor, 1.0, 1.0, 1.0, 1.0)
}
gl.ActiveTexture(gl.TEXTURE0)
gl.Uniform1i(shader.LocTexture, 0)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BindTexture(gl.TEXTURE_2D, selectedFont.Map.FontMapTextureID)
gl.BindVertexArray(fm.stringVAO)
gl.BindBuffer(gl.ARRAY_BUFFER, fm.stringVBO)
var currentX float32
atlasW, atlasH := float32(selectedFont.Map.Width), float32(selectedFont.Map.Height)
for _, glyph := range glyphsToRender {
xPos := currentX
yPos := float32(0)
w, h := float32(glyph.SrcRect.W), float32(glyph.SrcRect.H)
u0 := float32(glyph.SrcRect.X) / atlasW
v0 := float32(glyph.SrcRect.Y) / atlasH
u1 := u0 + w/atlasW
v1 := v0 + h/atlasH
vertices := []float32{
xPos, yPos + h, u0, v0,
xPos, yPos, u0, v1,
xPos + w, yPos, u1, v1,
xPos, yPos + h, u0, v0,
xPos + w, yPos, u1, v1,
xPos + w, yPos + h, u1, v0,
}
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(vertices)*4, gl.Ptr(vertices))
gl.DrawArrays(gl.TRIANGLES, 0, 6)
currentX += float32(glyph.Advance)
}
usedWidth := float32(currentX)
gl.BindVertexArray(0)
gl.BindFramebuffer(gl.FRAMEBUFFER, uint32(lastFBO))
gl.Viewport(lastViewport[0], lastViewport[1], lastViewport[2], lastViewport[3])
gl.Disable(gl.BLEND)
return newTextureID, usedWidth, maxHeight, nil
}
+128
View File
@@ -0,0 +1,128 @@
package graphics
import (
"fmt"
"log"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/veandco/go-sdl2/sdl"
)
// CreateTextureFromSurface uploads an SDL surface to an OpenGL texture and returns the texture ID.
func CreateTextureFromSurface(surface *sdl.Surface) (uint32, error) {
if surface == nil {
return 0, fmt.Errorf("CreateTextureFromSurface: input surface is nil")
}
var textureID uint32
var err error
surfaceToUpload := surface
var convertedSurface *sdl.Surface = nil
sdlFormatEnum := surface.Format.Format
var dataFormat uint32
var internalFormat int32 = gl.RGBA8
if sdlFormatEnum == sdl.PIXELFORMAT_INDEX8 {
convertedSurface, err = surface.ConvertFormat(sdl.PIXELFORMAT_ARGB8888, 0)
if err != nil {
return 0, fmt.Errorf("failed to convert INDEX8 surface to ARGB8888: %w", err)
}
defer convertedSurface.Free()
surfaceToUpload = convertedSurface
sdlFormatEnum = surfaceToUpload.Format.Format
}
switch sdlFormatEnum {
case sdl.PIXELFORMAT_ARGB8888:
dataFormat = gl.BGRA
case sdl.PIXELFORMAT_ABGR8888:
dataFormat = gl.RGBA
case sdl.PIXELFORMAT_RGBA8888:
dataFormat = gl.RGBA
case sdl.PIXELFORMAT_BGRA8888:
dataFormat = gl.BGRA
default:
return 0, fmt.Errorf("unhandled SDL pixel format for OpenGL upload: %s", sdl.GetPixelFormatName(uint(sdlFormatEnum)))
}
for errCode := gl.GetError(); errCode != gl.NO_ERROR; errCode = gl.GetError() {
log.Printf("OpenGL error pending before GenTextures: 0x%X", errCode)
}
gl.GenTextures(1, &textureID)
if errCode := gl.GetError(); errCode != gl.NO_ERROR {
return 0, fmt.Errorf("OpenGL error 0x%X after GenTextures", errCode)
}
if textureID == 0 {
return 0, fmt.Errorf("gl.GenTextures returned textureID 0")
}
gl.BindTexture(gl.TEXTURE_2D, textureID)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
expectedPitch := surfaceToUpload.W * int32(surfaceToUpload.Format.BytesPerPixel)
if int32(surfaceToUpload.Pitch) != expectedPitch {
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, surfaceToUpload.Pitch/int32(surfaceToUpload.Format.BytesPerPixel))
} else {
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
}
gl.TexImage2D(
gl.TEXTURE_2D,
0,
internalFormat,
surfaceToUpload.W,
surfaceToUpload.H,
0,
dataFormat,
gl.UNSIGNED_BYTE,
gl.Ptr(surfaceToUpload.Pixels()),
)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 4)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
if glErr := gl.GetError(); glErr != gl.NO_ERROR {
gl.DeleteTextures(1, &textureID)
return 0, fmt.Errorf("OpenGL error 0x%X after TexImage2D", glErr)
}
gl.BindTexture(gl.TEXTURE_2D, 0)
return textureID, nil
}
// CheckGLError queries OpenGL for errors and returns the first one found.
func CheckGLError() error {
errCode := gl.GetError()
if errCode == gl.NO_ERROR {
return nil
}
switch errCode {
case gl.INVALID_ENUM:
return fmt.Errorf("INVALID_ENUM")
case gl.INVALID_VALUE:
return fmt.Errorf("INVALID_VALUE")
case gl.INVALID_OPERATION:
return fmt.Errorf("INVALID_OPERATION")
case gl.STACK_OVERFLOW:
return fmt.Errorf("STACK_OVERFLOW")
case gl.STACK_UNDERFLOW:
return fmt.Errorf("STACK_UNDERFLOW")
case gl.OUT_OF_MEMORY:
return fmt.Errorf("OUT_OF_MEMORY")
case gl.INVALID_FRAMEBUFFER_OPERATION:
return fmt.Errorf("INVALID_FRAMEBUFFER_OPERATION")
default:
return fmt.Errorf("Unknown GL error: 0x%X", errCode)
}
}
+193
View File
@@ -0,0 +1,193 @@
package graphics
import (
"fmt"
"github.com/go-gl/gl/v3.3-core/gl"
)
const (
VirtualWidth int32 = 1920
VirtualHeight int32 = 1080
)
var (
vfbo uint32
vfboTexture uint32
vfboDepthStencil uint32
blitVAO uint32
blitVBO uint32
blitShaderProg uint32
nativeW, nativeH int32
)
// InitVirtualFramebuffer creates the FBO at virtual resolution and the blit shader.
// Call after OpenGL context is created.
func InitVirtualFramebuffer(displayW, displayH int32) error {
nativeW = displayW
nativeH = displayH
// --- FBO setup ---
gl.GenFramebuffers(1, &vfbo)
gl.BindFramebuffer(gl.FRAMEBUFFER, vfbo)
gl.GenTextures(1, &vfboTexture)
gl.BindTexture(gl.TEXTURE_2D, vfboTexture)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, VirtualWidth, VirtualHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, vfboTexture, 0)
// Depth/stencil renderbuffer
gl.GenRenderbuffers(1, &vfboDepthStencil)
gl.BindRenderbuffer(gl.RENDERBUFFER, vfboDepthStencil)
gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, VirtualWidth, VirtualHeight)
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, vfboDepthStencil)
if status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER); status != gl.FRAMEBUFFER_COMPLETE {
return fmt.Errorf("virtual framebuffer incomplete: 0x%X", status)
}
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
// --- Fullscreen quad ---
vertices := []float32{
// pos (x,y) texcoord (u,v)
-1, -1, 0, 0,
1, -1, 1, 0,
-1, 1, 0, 1,
1, 1, 1, 1,
}
gl.GenVertexArrays(1, &blitVAO)
gl.GenBuffers(1, &blitVBO)
gl.BindVertexArray(blitVAO)
gl.BindBuffer(gl.ARRAY_BUFFER, blitVBO)
gl.BufferData(gl.ARRAY_BUFFER, len(vertices)*4, gl.Ptr(vertices), gl.STATIC_DRAW)
gl.EnableVertexAttribArray(0)
gl.VertexAttribPointerWithOffset(0, 2, gl.FLOAT, false, 4*4, 0)
gl.EnableVertexAttribArray(1)
gl.VertexAttribPointerWithOffset(1, 2, gl.FLOAT, false, 4*4, 2*4)
gl.BindVertexArray(0)
// --- Blit shader ---
var err error
blitShaderProg, err = compileBlitShader()
if err != nil {
return fmt.Errorf("blit shader compilation failed: %w", err)
}
return nil
}
// BeginVirtualRender binds the FBO so all subsequent draws go to the virtual 1080p buffer.
func BeginVirtualRender() {
gl.BindFramebuffer(gl.FRAMEBUFFER, vfbo)
gl.Viewport(0, 0, VirtualWidth, VirtualHeight)
}
// BlitToScreen draws the virtual buffer to the actual screen at native resolution.
func BlitToScreen() {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
gl.Viewport(0, 0, nativeW, nativeH)
gl.ClearColor(0, 0, 0, 1)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.UseProgram(blitShaderProg)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, vfboTexture)
gl.BindVertexArray(blitVAO)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
gl.BindVertexArray(0)
}
// DestroyVirtualFramebuffer cleans up all FBO resources.
func DestroyVirtualFramebuffer() {
if blitShaderProg != 0 {
gl.DeleteProgram(blitShaderProg)
}
if blitVBO != 0 {
gl.DeleteBuffers(1, &blitVBO)
}
if blitVAO != 0 {
gl.DeleteVertexArrays(1, &blitVAO)
}
if vfboDepthStencil != 0 {
gl.DeleteRenderbuffers(1, &vfboDepthStencil)
}
if vfboTexture != 0 {
gl.DeleteTextures(1, &vfboTexture)
}
if vfbo != 0 {
gl.DeleteFramebuffers(1, &vfbo)
}
}
func compileBlitShader() (uint32, error) {
vertSrc := `#version 330 core
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aTexCoord;
out vec2 vTexCoord;
void main() {
vTexCoord = aTexCoord;
gl_Position = vec4(aPos, 0.0, 1.0);
}` + "\x00"
fragSrc := `#version 330 core
in vec2 vTexCoord;
out vec4 FragColor;
uniform sampler2D screenTexture;
void main() {
FragColor = texture(screenTexture, vTexCoord);
}` + "\x00"
vertShader, err := compileShader(vertSrc, gl.VERTEX_SHADER)
if err != nil {
return 0, err
}
fragShader, err := compileShader(fragSrc, gl.FRAGMENT_SHADER)
if err != nil {
return 0, err
}
prog := gl.CreateProgram()
gl.AttachShader(prog, vertShader)
gl.AttachShader(prog, fragShader)
gl.LinkProgram(prog)
var status int32
gl.GetProgramiv(prog, gl.LINK_STATUS, &status)
if status == gl.FALSE {
var logLen int32
gl.GetProgramiv(prog, gl.INFO_LOG_LENGTH, &logLen)
logStr := make([]byte, logLen)
gl.GetProgramInfoLog(prog, logLen, nil, &logStr[0])
return 0, fmt.Errorf("shader link error: %s", string(logStr))
}
gl.DeleteShader(vertShader)
gl.DeleteShader(fragShader)
return prog, nil
}
func compileShader(src string, shaderType uint32) (uint32, error) {
shader := gl.CreateShader(shaderType)
csrc, free := gl.Strs(src)
gl.ShaderSource(shader, 1, csrc, nil)
free()
gl.CompileShader(shader)
var status int32
gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
if status == gl.FALSE {
var logLen int32
gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLen)
logStr := make([]byte, logLen)
gl.GetShaderInfoLog(shader, logLen, nil, &logStr[0])
return 0, fmt.Errorf("shader compile error: %s", string(logStr))
}
return shader, nil
}
+79
View File
@@ -0,0 +1,79 @@
// package keybinds provides keybind functionality for the entire application
package keybinds
import (
"github.com/veandco/go-sdl2/sdl"
)
// KeyCombo represents a combination of a key and it's active modifier.
type KeyCombo struct {
Key sdl.Keycode
Mod sdl.Keymod
}
// Binds contains all currently registered Keybinds.
var Binds = make(map[KeyCombo]func())
// Enabled controls whether PerformAction dispatches keybinds. Set to false
// to temporarily suppress all global keybinds (e.g. during a modal page).
var Enabled = true
// Regiser registers a new keybind in Binds
func Register(k sdl.Keycode, m sdl.Keymod, action func()) {
mod := NormalizeModifiers(m)
newCombo := KeyCombo{k, mod}
_, ok := Binds[newCombo]
if ok {
}
Binds[newCombo] = action
}
// BindKey adds a new KeyCombo and Function (the action to take) to the keybinds.Binds
func BindKey(k KeyCombo, f func()) {
k.Mod = NormalizeModifiers(k.Mod)
Binds[k] = f
}
// UnbindKey removes a KeyCombo from the available binds
func UnbindKey(k KeyCombo) {
k.Mod = NormalizeModifiers(k.Mod)
delete(Binds, k)
}
func PerformAction(event sdl.Event) {
if !Enabled {
return
}
switch e := event.(type) {
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
key := e.Keysym.Sym
rawMod := e.Keysym.Mod
mod := NormalizeModifiers(sdl.Keymod(rawMod))
combo := KeyCombo{key, mod}
action, ok := Binds[combo]
if ok {
action()
}
}
}
}
// NormalizeModifiers converts a raw SDL modifier bitmask into a canonical form
// focusing on common modifiers (SHIFT, CTRL, ALT, GUI) and ignoring others like CapsLock.
func NormalizeModifiers(mod sdl.Keymod) sdl.Keymod {
var normalized sdl.Keymod = sdl.KMOD_NONE
if mod&sdl.KMOD_SHIFT != 0 {
normalized |= sdl.KMOD_SHIFT
}
if mod&sdl.KMOD_CTRL != 0 {
normalized |= sdl.KMOD_CTRL
}
if mod&sdl.KMOD_ALT != 0 {
normalized |= sdl.KMOD_ALT
}
if mod&sdl.KMOD_GUI != 0 {
normalized |= sdl.KMOD_GUI
}
return normalized
}
+73
View File
@@ -0,0 +1,73 @@
package license
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"time"
)
var Level int
type Validator struct {
client *http.Client
apiURL string
apiPath string
}
type Response struct {
Key string `json:"key"`
Email string `json:"email"`
Active bool `json:"active"`
Type string `json:"type"`
Level string `json:"level"`
LastCheck time.Time `json:"lastCheck"`
NewCheck time.Time `json:"newCheck"`
}
func NewValidator(apiURL, apiPath string) *Validator {
return &Validator{
client: &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
},
},
apiURL: apiURL,
apiPath: apiPath,
}
}
func (v *Validator) Validate(key, email string) (*Response, error) {
u, err := url.Parse(v.apiURL)
if err != nil {
return nil, err
}
u.Path = path.Join(u.Path, v.apiPath)
q := u.Query()
q.Set("licenseKey", key)
q.Set("emailAddress", email)
u.RawQuery = q.Encode()
resp, err := v.client.Get(u.String())
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %w", err)
}
var result Response
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed ot decode response: %w", err)
}
return &result, nil
}
+138
View File
@@ -0,0 +1,138 @@
package logger
import (
"fmt"
"log"
"os"
"sync"
"time"
"gorm.io/gorm"
)
type Level int
const (
LevelDebug Level = iota
LevelInfo
LevelWarning
LevelError
LevelFatal
)
func (l Level) String() string {
return [...]string{"DEBUG", "INFO", "WARNING", "ERROR", "FATAL"}[l]
}
type logEntry struct {
timestamp string
level string
message string
}
var (
mu sync.Mutex
db *gorm.DB
file *os.File
console = log.New(os.Stderr, "", 0)
dbChan chan logEntry
dbChanOnce sync.Once
)
// startDBWriter starts a single background goroutine to drain log entries to the database.
func startDBWriter() {
dbChan = make(chan logEntry, 256)
go func() {
for entry := range dbChan {
row := map[string]any{
"timestamp": entry.timestamp,
"level": entry.level,
"message": entry.message,
}
if err := db.Table("log_entries").Create(row).Error; err != nil {
mu.Lock()
console.Printf("Failed to write log to database: %v", err)
mu.Unlock()
}
}
}()
}
// AddFile attaches or replaces the file backend
func AddFile(path string) error {
mu.Lock()
defer mu.Unlock()
if file != nil {
file.Close()
}
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
file = f
return nil
}
// AddDatabase attaches or replaces the database backend
func AddDatabase(d *gorm.DB) {
mu.Lock()
defer mu.Unlock()
db = d
dbChanOnce.Do(startDBWriter)
}
// Close shuts down the database writer goroutine and closes the file backend.
func Close() error {
mu.Lock()
defer mu.Unlock()
// Stop the DB writer goroutine by closing the channel
if dbChan != nil {
close(dbChan)
dbChan = nil
db = nil
}
if file != nil {
err := file.Close()
file = nil
return err
}
return nil
}
func logMsg(level Level, format string, args ...any) {
message := fmt.Sprintf(format, args...)
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
line := fmt.Sprintf("%s [%s] %s", timestamp, level, message)
mu.Lock()
console.Println(line)
if file != nil {
fmt.Fprintln(file, line)
}
if db != nil && dbChan != nil { // both cleared by Close()
select {
case dbChan <- logEntry{timestamp: timestamp, level: level.String(), message: message}:
default:
// Channel full, drop the DB write rather than blocking
}
}
mu.Unlock()
}
func Debug(format string, args ...any) { logMsg(LevelDebug, format, args...) }
func Info(format string, args ...any) { logMsg(LevelInfo, format, args...) }
func Warning(format string, args ...any) { logMsg(LevelWarning, format, args...) }
func Error(format string, args ...any) { logMsg(LevelError, format, args...) }
func Fatal(format string, args ...any) {
logMsg(LevelFatal, format, args...)
time.Sleep(100 * time.Millisecond)
os.Exit(1)
}
+346
View File
@@ -0,0 +1,346 @@
// internal/audio/mixer/mixer.go
package mixer
import (
"errors"
"fmt"
"strconv"
"sync"
"github.com/veandco/go-sdl2/mix"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/logger"
)
var (
mu sync.Mutex
inited bool
deviceOpen bool
deviceName string
currentMusic *mix.Music
currentPath string
currentLoops int // 0 = once, -1 = forever
currentVolume int // 0..128, defaults to MAX_VOLUME
// Config knobs (could be made public or read from your config)
defaultFreq = 48000
defaultFormat = uint16(sdl.AUDIO_F32)
defaultChannels = 2
defaultChunkSize = 1024
defaultAllow = sdl.AUDIO_ALLOW_ANY_CHANGE
)
// Init initializes SDL audio + SDL_mixer. Safe to call multiple times.
func Init(wantedDevice string) error {
mu.Lock()
defer mu.Unlock()
if inited {
return nil
}
// Let SDL choose the best audio driver for the system
// Remove hardcoded ALSA preference to support PulseAudio, PipeWire, etc.
// Users can set SDL_AUDIODRIVER environment variable if needed
if err := sdl.InitSubSystem(sdl.INIT_AUDIO); err != nil {
return fmt.Errorf("SDL audio init failed: %w", err)
}
// WAV-only does not require mix.Init(); add when you support OGG/MP3/etc.
if err := openDeviceLocked(wantedDevice); err != nil {
// Clean up the subsystem on failure so callers can retry later.
sdl.QuitSubSystem(sdl.INIT_AUDIO)
return err
}
// Check if the volume is stored in the config; if not, default to max.
storedVolume, err := db.GetConfigValue("last_volume")
if err != nil {
return fmt.Errorf("retrieving last_volume from config: %w", err)
}
lastVolume, err := strconv.Atoi(storedVolume)
if err != nil {
return fmt.Errorf("parsing last_volume %q: %w", storedVolume, err)
}
if lastVolume < 0 {
lastVolume = int(mix.MAX_VOLUME)
currentVolume = lastVolume
if err := db.SetConfigValue("last_volume", strconv.Itoa(lastVolume)); err != nil {
}
} else {
currentVolume = lastVolume
}
// mix.VolumeMusic(currentVolume)
inited = true
return nil
}
// openDeviceLocked tries to open the named device; if it fails and the name wasn't empty, falls back to default.
func openDeviceLocked(name string) error {
if err := mix.OpenAudioDevice(defaultFreq, defaultFormat, defaultChannels, defaultChunkSize, name, defaultAllow); err != nil {
if name != "" {
// Log the failure of the requested device
// Fallback to default device
if err2 := mix.OpenAudioDevice(defaultFreq, defaultFormat, defaultChannels, defaultChunkSize, "", defaultAllow); err2 != nil {
return fmt.Errorf("failed to open device %q and default device: %w", name, err2)
}
deviceName = "" // actually opened default
} else {
return fmt.Errorf("failed to open default audio device: %w", err)
}
} else {
if name != "" {
} else {
}
deviceName = name
}
deviceOpen = true
return nil
}
// 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) error {
if !db.AudioEnabled() {
return nil
}
mu.Lock()
defer mu.Unlock()
if !inited || !deviceOpen {
return errors.New("audio mixer not initialized")
}
if path == "" {
return errors.New("empty audio path")
}
// Stop and free any prior music
mix.HaltMusic()
if currentMusic != nil {
currentMusic.Free()
currentMusic = nil
}
m, err := mix.LoadMUS(path)
if err != nil {
return fmt.Errorf("LoadMUS(%q): %w", path, err)
}
currentMusic = m
currentPath = path
if loopForever {
currentLoops = -1
} else {
currentLoops = 0
}
// Open the device
if err := openDeviceLocked(deviceName); err != nil {
logger.Error("Failed to open device: %w", err)
}
// Apply remembered volume
mix.VolumeMusic(currentVolume)
if err := currentMusic.Play(currentLoops); err != nil {
currentMusic.Free()
currentMusic = nil
return fmt.Errorf("music.Play: %w", err)
}
return nil
}
// Stop stops playback. If fadeMs > 0, fades out asynchronously; otherwise halts immediately.
// It also frees the currently loaded music (so a new Play will reload).
func Stop(fadeMs int) {
if !db.AudioEnabled() {
return
}
mu.Lock()
defer mu.Unlock()
if !inited {
return
}
if fadeMs > 0 && mix.PlayingMusic() {
_ = mix.FadeOutMusic(fadeMs) // async fade; we’ll still free below
}
mix.HaltMusic()
if currentMusic != nil {
currentMusic.Free()
currentMusic = nil
}
currentPath = ""
currentLoops = 0
}
// SetVolume128 sets music volume using SDL_mixer’s 0..128 scale.
func SetVolume128(increment int) {
if !db.AudioEnabled() {
return
}
mu.Lock()
defer mu.Unlock()
currentVolume += increment
if currentVolume < 0 {
currentVolume = 0
} else if currentVolume > mix.MAX_VOLUME {
currentVolume = int(mix.MAX_VOLUME)
}
// Store in config
if err := db.SetConfigValue("last_volume", strconv.Itoa(currentVolume)); err != nil {
}
// Apply to SDL_mixer
if inited && deviceOpen {
mix.VolumeMusic(currentVolume)
}
}
// IsPlaying reports whether music is currently playing (or fading).
func IsPlaying() bool {
mu.Lock()
defer mu.Unlock()
if !inited {
return false
}
return mix.PlayingMusic()
}
// ListDevices returns the list of available output device names (from core SDL).
func ListDevices() []string {
mu.Lock()
defer mu.Unlock()
n := sdl.GetNumAudioDevices(false)
if n <= 0 {
return nil
}
out := make([]string, 0, n)
for i := 0; i < n; i++ {
if name := sdl.GetAudioDeviceName(i, false); name != "" {
out = append(out, name)
}
}
return out
}
// CurrentDevice returns the last requested/opened device name ("" means default).
func CurrentDevice() string {
mu.Lock()
defer mu.Unlock()
return deviceName
}
// SwitchDevice switches output devices, preserving volume and resuming playback of the current track/loop state if any.
func SwitchDevice(newName string) error {
mu.Lock()
defer mu.Unlock()
if !inited {
return errors.New("audio mixer not initialized")
}
// Log device switch attempt
oldDevice := deviceName
if oldDevice == "" {
oldDevice = "system default"
}
newDeviceLog := newName
if newDeviceLog == "" {
newDeviceLog = "system default"
}
// Capture current state
wasPlaying := mix.PlayingMusic()
savedPath := currentPath
savedLoops := currentLoops
savedVolume := currentVolume
// Stop and release current music before reopening device
if wasPlaying {
}
mix.HaltMusic()
if currentMusic != nil {
currentMusic.Free()
currentMusic = nil
}
currentPath = ""
currentLoops = 0
// Close and reopen device
if deviceOpen {
mix.CloseAudio()
deviceOpen = false
}
if err := openDeviceLocked(newName); err != nil {
// Best effort: try to reopen the previous device (or default) so we don't leave audio dead
if restoreErr := openDeviceLocked(oldDevice); restoreErr != nil {
} else {
}
return err
}
// Restore volume
mix.VolumeMusic(savedVolume)
// Optionally resume previous track if there was one
if wasPlaying && savedPath != "" {
m, err := mix.LoadMUS(savedPath)
if err != nil {
// Don't fail the device switch just because reload failed
return fmt.Errorf("device switched, but reloading %q failed: %w", savedPath, err)
}
currentMusic = m
currentPath = savedPath
currentLoops = savedLoops
if err := currentMusic.Play(currentLoops); err != nil {
currentMusic.Free()
currentMusic = nil
currentPath = ""
currentLoops = 0
return fmt.Errorf("device switched, but restarting playback failed: %w", err)
}
}
return nil
}
func GetVolume128() int {
mu.Lock()
defer mu.Unlock()
return currentVolume
}
// Shutdown fully tears everything down. Call at app exit.
func Shutdown() {
mu.Lock()
defer mu.Unlock()
mix.HaltMusic()
if currentMusic != nil {
currentMusic.Free()
currentMusic = nil
}
if deviceOpen {
mix.CloseAudio()
deviceOpen = false
}
// If you add mix.Init(decoderFlags) in the future, pair it with mix.Quit() here.
if inited {
sdl.QuitSubSystem(sdl.INIT_AUDIO)
inited = false
}
}
+428
View File
@@ -0,0 +1,428 @@
// Package network provides a high-level utility for managing network
// connections via the system's D-Bus and NetworkManager.
package network
import (
"errors"
"fmt"
"log"
"net"
"sort"
"strings"
"time"
"github.com/godbus/dbus/v5"
"github.com/google/uuid"
)
// --- D-Bus NetworkManager Constants ---
const (
nmService = "org.freedesktop.NetworkManager"
nmPath = "/org/freedesktop/NetworkManager"
nmInterface = "org.freedesktop.NetworkManager"
nmDeviceInterface = "org.freedesktop.NetworkManager.Device"
nmWirelessDeviceInterface = "org.freedesktop.NetworkManager.Device.Wireless"
nmAPInterface = "org.freedesktop.NetworkManager.AccessPoint"
nmDeviceTypeEthernet uint32 = 1
nmDeviceTypeWifi uint32 = 2
nmDeviceStateActivated uint32 = 100
APFlagsPrivacy uint32 = 0x00000001 // Equivalent to NM_802_11_AP_FLAGS_PRIVACY
APSecKeyMgmtEAP uint32 = 0x00000020
APSecKeyMgmtPSK uint32 = 0x00000100
APSecKeyMgmtOWE uint32 = 0x00000200
APSecKeyMgmtSAE uint32 = 0x00000400
)
// ErrScanAborted is returned when a scan is cancelled via the stop channel.
var ErrScanAborted = errors.New("network scan aborted by caller")
// Status defines the type for various connection status codes.
type Status int
const (
StatusError Status = iota
StatusNoDevicesFound
StatusWifiAvailableNotConnected
StatusWifiConnectedNoInternet
StatusWifiConnectedInternetUp
StatusEthernetNotConnected
StatusEthernetConnectedNoInternet
StatusEthernetConnectedInternetUp
)
// ConnectionStatus holds the detailed result of a connectivity check.
type ConnectionStatus struct {
StatusCode Status
Message string
}
// DeviceType defines the kind of network device.
type DeviceType int
const (
TypeUnknown DeviceType = iota
TypeEthernet
TypeWifi
)
// Device holds information about a single network device.
type Device struct {
Path dbus.ObjectPath
InterfaceName string
Type DeviceType
State uint32
}
// Manager handles all D-Bus communication with NetworkManager.
type Manager struct {
conn *dbus.Conn
nmObject dbus.BusObject
Devices []Device
}
// AccessPoint holds essential information about a discovered Wi-Fi network.
type AccessPoint struct {
ObjectPath dbus.ObjectPath
SSID string
Strength byte
IsProtected bool
IsActive bool
WpaFlags uint32
RsnFlags uint32
}
// New creates a new network Manager and connects to the system D-Bus.
func New() (*Manager, error) {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
}
nmObject := conn.Object(nmService, nmPath)
return &Manager{conn: conn, nmObject: nmObject}, nil
}
// Conn returns the underlying dbus.Conn for advanced queries.
func (m *Manager) Conn() *dbus.Conn {
return m.conn
}
// Close disconnects from the D-Bus.
func (m *Manager) Close() {
if m.conn != nil {
m.conn.Close()
}
}
// FindDevices scans the system for all available network devices (Ethernet and Wi-Fi).
func (m *Manager) FindDevices() error {
var devicePaths []dbus.ObjectPath
call := m.nmObject.Call(nmInterface+".GetDevices", 0)
if call.Err != nil {
return fmt.Errorf("GetDevices D-Bus call failed: %w", call.Err)
}
if err := call.Store(&devicePaths); err != nil {
return fmt.Errorf("could not parse device list: %w", err)
}
m.Devices = []Device{}
for _, path := range devicePaths {
deviceObj := m.conn.Object(nmService, path)
typeVariant, err := deviceObj.GetProperty(nmDeviceInterface + ".DeviceType")
if err != nil {
log.Printf("network: Skipping device %s, could not get DeviceType: %v", path, err)
continue
}
deviceType, _ := typeVariant.Value().(uint32)
var newDevice Device
switch deviceType {
case nmDeviceTypeEthernet:
newDevice.Type = TypeEthernet
case nmDeviceTypeWifi:
newDevice.Type = TypeWifi
default:
continue
}
newDevice.Path = path
ifaceVariant, _ := deviceObj.GetProperty(nmDeviceInterface + ".Interface")
newDevice.InterfaceName, _ = ifaceVariant.Value().(string)
stateVariant, _ := deviceObj.GetProperty(nmDeviceInterface + ".State")
newDevice.State, _ = stateVariant.Value().(uint32)
m.Devices = append(m.Devices, newDevice)
}
return nil
}
// CheckInternetConnection performs a comprehensive check for an active internet connection.
func (m *Manager) CheckInternetConnection() (*ConnectionStatus, error) {
if err := m.FindDevices(); err != nil {
return &ConnectionStatus{StatusError, "Failed to scan for network devices"}, err
}
if len(m.Devices) == 0 {
return &ConnectionStatus{StatusNoDevicesFound, "No network hardware found."}, nil
}
hasWifiAdapter := false
hasEthernetAdapter := false
for _, device := range m.Devices {
if device.Type == TypeEthernet {
hasEthernetAdapter = true
if device.State == nmDeviceStateActivated {
if pingTest() {
return &ConnectionStatus{StatusEthernetConnectedInternetUp, "Ethernet connected with internet access."}, nil
}
return &ConnectionStatus{StatusEthernetConnectedNoInternet, "Ethernet connected, but no internet access."}, nil
}
}
}
for _, device := range m.Devices {
if device.Type == TypeWifi {
hasWifiAdapter = true
if device.State == nmDeviceStateActivated {
if pingTest() {
return &ConnectionStatus{StatusWifiConnectedInternetUp, "Wi-Fi connected with internet access."}, nil
}
return &ConnectionStatus{StatusWifiConnectedNoInternet, "Wi-Fi connected, but no internet access."}, nil
}
}
}
if hasWifiAdapter {
return &ConnectionStatus{StatusWifiAvailableNotConnected, "Wi-Fi adapter is available but not connected."}, nil
}
if hasEthernetAdapter {
return &ConnectionStatus{StatusEthernetNotConnected, "Ethernet adapter is available but not connected."}, nil
}
return &ConnectionStatus{StatusNoDevicesFound, "No recognizable network hardware found."}, nil
}
// StartScan performs a Wi-Fi scan asynchronously. The optional stopChan can be used to abort.
func (m *Manager) StartScan(resultsChan chan<- []AccessPoint, errorChan chan<- error, stopChan <-chan struct{}) {
var wifiDevice *Device
for i := range m.Devices {
if m.Devices[i].Type == TypeWifi {
wifiDevice = &m.Devices[i]
break
}
}
if wifiDevice == nil {
errorChan <- fmt.Errorf("no Wi-Fi device available for scanning")
return
}
// Ensure Wi-Fi is enabled
nmObj := m.conn.Object(nmService, nmPath)
prop, err := nmObj.GetProperty(nmInterface + ".WirelessEnabled")
if err != nil {
errorChan <- fmt.Errorf("could not read WirelessEnabled property: %w", err)
return
}
if !prop.Value().(bool) {
call := nmObj.Call("org.freedesktop.DBus.Properties.Set", 0,
nmInterface,
"WirelessEnabled",
dbus.MakeVariant(true),
)
if call.Err != nil {
errorChan <- fmt.Errorf("could not enable Wi-Fi: %w", call.Err)
return
}
time.Sleep(2 * time.Second) // Give NetworkManager time to bring up the device
}
// Request a scan
wifiDeviceWireless := m.conn.Object(nmService, wifiDevice.Path)
call := wifiDeviceWireless.Call(nmWirelessDeviceInterface+".RequestScan", 0, make(map[string]dbus.Variant))
if call.Err != nil {
errorChan <- fmt.Errorf("scan request failed: %w", call.Err)
return
}
select {
case <-time.After(3 * time.Second):
case <-stopChan:
errorChan <- ErrScanAborted
return
}
var apPaths []dbus.ObjectPath
call = wifiDeviceWireless.Call(nmWirelessDeviceInterface+".GetAllAccessPoints", 0)
if call.Err != nil {
errorChan <- fmt.Errorf("could not retrieve network list: %w", call.Err)
return
}
if err := call.Store(&apPaths); err != nil {
errorChan <- fmt.Errorf("could not parse network list: %w", err)
return
}
processedAPs, err := m.scanAccessPoints(apPaths)
if err != nil {
errorChan <- err
return
}
resultsChan <- processedAPs
}
// scanAccessPoints filters and sorts the raw list of D-Bus AP paths.
func (m *Manager) scanAccessPoints(apPaths []dbus.ObjectPath) ([]AccessPoint, error) {
tempNetworkMap := make(map[string]AccessPoint)
for _, apPath := range apPaths {
apObj := m.conn.Object(nmService, apPath)
props, err := m.getAllProperties(apObj, nmAPInterface)
if err != nil {
log.Printf("network: Failed to get properties for AP %s: %v", apPath, err)
continue
}
ssidBytes, _ := props["Ssid"].Value().([]byte)
if len(ssidBytes) == 0 {
continue
}
ssidStr := string(ssidBytes)
strength, _ := props["Strength"].Value().(byte)
flags, _ := props["Flags"].Value().(uint32)
wpaFlags, _ := props["WpaFlags"].Value().(uint32)
rsnFlags, _ := props["RsnFlags"].Value().(uint32)
isProtected := (flags&APFlagsPrivacy != 0) || (wpaFlags != 0) || (rsnFlags != 0)
currentAP := AccessPoint{
ObjectPath: apPath,
SSID: ssidStr,
Strength: strength,
IsProtected: isProtected,
WpaFlags: wpaFlags,
RsnFlags: rsnFlags,
}
if existingAP, ok := tempNetworkMap[ssidStr]; !ok || strength > existingAP.Strength {
tempNetworkMap[ssidStr] = currentAP
}
}
networkList := make([]AccessPoint, 0, len(tempNetworkMap))
for _, ap := range tempNetworkMap {
networkList = append(networkList, ap)
}
sort.Slice(networkList, func(i, j int) bool {
if networkList[i].Strength != networkList[j].Strength {
return networkList[i].Strength > networkList[j].Strength
}
return strings.ToLower(networkList[i].SSID) < strings.ToLower(networkList[j].SSID)
})
return networkList, nil
}
// Connect attempts to connect a Wi-Fi device to a given access point.
func (m *Manager) Connect(wifiDevice Device, ap AccessPoint, passphrase string) error {
if wifiDevice.Type != TypeWifi {
return fmt.Errorf("cannot connect: provided device is not a Wi-Fi device")
}
settings := map[string]map[string]dbus.Variant{
"connection": {
"type": dbus.MakeVariant("802-11-wireless"),
"uuid": dbus.MakeVariant(uuid.NewString()),
"id": dbus.MakeVariant(ap.SSID),
"interface-name": dbus.MakeVariant(wifiDevice.InterfaceName),
},
"802-11-wireless": {
"ssid": dbus.MakeVariant([]byte(ap.SSID)),
"mode": dbus.MakeVariant("infrastructure"),
},
}
if ap.IsProtected {
settings["802-11-wireless"]["security"] = dbus.MakeVariant("802-11-wireless-security")
secSettings := make(map[string]dbus.Variant)
// Default to WPA-PSK, but override with more specific types if found.
secSettings["key-mgmt"] = dbus.MakeVariant("wpa-psk")
if (ap.RsnFlags & APSecKeyMgmtSAE) != 0 {
secSettings["key-mgmt"] = dbus.MakeVariant("sae") // WPA3
} else if (ap.RsnFlags&APSecKeyMgmtEAP) != 0 || (ap.WpaFlags&APSecKeyMgmtEAP) != 0 {
log.Println("network: Enterprise AP (EAP) detected. Full configuration may be required.")
secSettings["key-mgmt"] = dbus.MakeVariant("wpa-eap")
}
secSettings["psk"] = dbus.MakeVariant(passphrase)
settings["802-11-wireless-security"] = secSettings
}
call := m.nmObject.Call(nmInterface+".AddAndActivateConnection", 0, settings, wifiDevice.Path, ap.ObjectPath)
if call.Err != nil {
return fmt.Errorf("AddAndActivateConnection D-Bus call error: %w", call.Err)
}
return nil
}
// pingTest performs a simple DNS lookup to check for internet connectivity.
func pingTest() bool {
_, err := net.LookupHost("www.google.com")
return err == nil
}
// getAllProperties is a helper to fetch all properties for a D-Bus object interface.
func (m *Manager) getAllProperties(obj dbus.BusObject, iface string) (map[string]dbus.Variant, error) {
var props map[string]dbus.Variant
call := obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, iface)
if call.Err != nil {
return nil, call.Err
}
if err := call.Store(&props); err != nil {
return nil, err
}
return props, nil
}
// WaitForInternet blocks until CheckInternetConnection reports Internet up,
// or until maxWait elapses. When it first sees "up", it requires that state
// to be stable for at least 'stableFor' before returning success to avoid flapping.
func (m *Manager) WaitForInternet(maxWait, stableFor time.Duration) (*ConnectionStatus, error) {
deadline := time.Now().Add(maxWait)
var lastErr error
var lastStatus *ConnectionStatus
var upSince time.Time
// Reasonable poll cadence; NM typically completes within a few seconds.
const poll = 500 * time.Millisecond
for time.Now().Before(deadline) {
st, err := m.CheckInternetConnection()
if err != nil {
lastErr = err
lastStatus = &ConnectionStatus{StatusError, "NetworkManager not ready"}
upSince = time.Time{} // reset stability
} else {
lastStatus = st
switch st.StatusCode {
case StatusEthernetConnectedInternetUp, StatusWifiConnectedInternetUp:
if upSince.IsZero() {
upSince = time.Now()
}
if time.Since(upSince) >= stableFor {
return st, nil // stable "up"
}
default:
// not up; keep waiting
upSince = time.Time{}
}
}
time.Sleep(poll)
}
// Timed out; return the last observed status and error (if any).
if lastStatus == nil {
lastStatus = &ConnectionStatus{StatusError, "Timed out waiting for connectivity"}
}
if lastErr != nil {
return lastStatus, fmt.Errorf("timeout waiting for internet: %w", lastErr)
}
return lastStatus, nil
}
+273
View File
@@ -0,0 +1,273 @@
package page
import (
"fmt"
"strconv"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/mixer"
"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
}
func (p *AudioDevices) Init(app ApplicationInterface) error {
var err error
p.verticalMargin = int32(20)
// Get font
fm := fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return fmt.Errorf("failed to get font: %w", sdl.GetError())
}
p.font = font
// Get shaders
sm := shaderManager.Get()
p.textShader, ok = sm.Get("text")
if !ok {
return fmt.Errorf("failed to get text shader: %w", sdl.GetError())
}
p.solidShader, ok = sm.Get("solid_color")
if !ok {
return fmt.Errorf("failed to get solid_color shader: %w", sdl.GetError())
}
// Initialize base
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("base init failed: %w", err)
}
// Get font size from database
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size %q: %w", baseFontSizeStr, err)
}
// Calculate scales
titleScale := float32(64) / float32(baseFontSize)
itemScale := float32(32) / float32(baseFontSize)
// Create title
p.title, err = NewStringItem("Select an Audio Output Device", 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)
if err != nil {
return fmt.Errorf("creating prompt: %w", err)
}
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, err := db.GetConfigValue("audio_device_name")
if err != nil {
return fmt.Errorf("retrieving audio_device_name from db: %w", err)
}
// 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, err := db.GetConfigValue("audio_enabled")
if err != nil {
return fmt.Errorf("unable to query database for audio_enabled: %w", err)
}
audioStatus := "Turn Audio On"
if on == "true" {
audioStatus = "Turn Audio Off"
}
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.devices = append(p.devices, audioStatusStringItem)
p.layout()
return nil
}
func (p *AudioDevices) layout() {
// Position title at bottom
p.title.X = p.ScreenCenterX - p.title.W/2
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.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 {
e, ok := (*event).(*sdl.KeyboardEvent)
if !ok || e.Type != sdl.KEYDOWN {
return nil
}
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" {
on, err := db.GetConfigValue("audio_enabled")
if err != nil {
return fmt.Errorf("failed to fetch audio_enabled from database: %w", err)
}
currentStatus, err := strconv.ParseBool(on)
if err != nil {
return fmt.Errorf("failed to parse audio_enabled value: %w", err)
}
newStatus := strconv.FormatBool(!currentStatus)
if err := db.SetConfigValue("audio_enabled", newStatus); err != nil {
return fmt.Errorf("failed to update audio_enabled: %w", err)
}
p.App.UnwindToPage(&Settings{})
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
if err := db.SetConfigValue("audio_device_name", deviceName); err != nil {
return fmt.Errorf("failed to save audio device: %w", err)
}
if err := db.SetConfigValue("audio_enabled", "true"); err != nil {
return fmt.Errorf("failed to enable audio: %w", err)
}
p.App.UnwindToPage(&Settings{})
case sdl.K_BACKSPACE:
p.App.UnwindToPage(&Settings{})
}
return nil
}
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)
// 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,
)
// 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)
}
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
}
+276
View File
@@ -0,0 +1,276 @@
package page
import (
"fmt"
"strconv"
"sync/atomic"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/keybinds"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type downloadState int
const (
downloadStateDownloading downloadState = iota
downloadStateFinished
)
// DownloadProgressPage displays a progress bar while an audio file is downloading.
// The page is inescapable: no input is accepted while downloading, and only
// Enter is accepted once the download finishes.
type DownloadProgressPage struct {
Base
textShader *shaderManager.Shader
solidShader *shaderManager.Shader
// Set by the application before pushing this page.
TotalBytes *atomic.Int64
BytesWritten *atomic.Int64
Done *atomic.Bool
HasError *atomic.Bool
NextPage Page // where to navigate on success
state downloadState
title StringItem
prompt StringItem
percentText StringItem
lastPercent int
barX, barY, barW, barH int32
}
func (p *DownloadProgressPage) Init(app ApplicationInterface) error {
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
fm := fontManager.Get()
sm := shaderManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return fmt.Errorf("failed to get font")
}
p.textShader, ok = sm.Get("text")
if !ok {
return fmt.Errorf("failed to get text shader")
}
p.solidShader, ok = sm.Get("solid_color")
if !ok {
return fmt.Errorf("failed to get solid_color shader")
}
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
titleScale := float32(48) / float32(baseFontSize)
p.title, err = NewStringItem("Downloading Audio...", font, colors.White)
if err != nil {
return fmt.Errorf("creating title: %w", err)
}
p.title.Scale(titleScale)
promptScale := float32(32) / float32(baseFontSize)
p.prompt, err = NewStringItem("Press Enter to continue", font, colors.White)
if err != nil {
return fmt.Errorf("creating prompt: %w", err)
}
p.prompt.Scale(promptScale)
p.percentText, err = NewStringItem("0%", font, colors.White)
if err != nil {
return fmt.Errorf("creating percent text: %w", err)
}
p.percentText.Scale(promptScale)
p.lastPercent = 0
// Progress bar dimensions: 60% of screen width, 30px tall, centered
p.barW = int32(float32(p.ScreenWidth) * 0.6)
p.barH = 30
p.barX = p.ScreenCenterX - p.barW/2
p.barY = p.ScreenCenterY - p.barH/2
// Position title above bar
p.title.X = p.ScreenCenterX - p.title.W/2
p.title.Y = p.barY + p.barH + 40
p.state = downloadStateDownloading
keybinds.Enabled = false
return nil
}
func (p *DownloadProgressPage) Update(dt float32) error {
if p.state == downloadStateFinished {
return nil
}
if p.Done.Load() {
p.state = downloadStateFinished
// Force progress to 100% on success
if !p.HasError.Load() && p.TotalBytes.Load() > 0 {
p.BytesWritten.Store(p.TotalBytes.Load())
p.lastPercent = 100
fm := fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if ok {
gl.DeleteTextures(1, &p.percentText.ID)
baseFontSizeStr, _ := db.GetConfigValue("init_font_size")
baseFontSize, _ := strconv.Atoi(baseFontSizeStr)
percentScale := float32(32) / float32(baseFontSize)
newText, err := NewStringItem("100%", font, colors.White)
if err == nil {
newText.Scale(percentScale)
p.percentText = newText
}
}
}
// Update title to reflect result
fm := fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return nil
}
baseFontSizeStr, _ := db.GetConfigValue("init_font_size")
baseFontSize, _ := strconv.Atoi(baseFontSizeStr)
titleScale := float32(48) / float32(baseFontSize)
gl.DeleteTextures(1, &p.title.ID)
var titleText string
if p.HasError.Load() {
titleText = "Download Failed"
} else {
titleText = "Download Complete"
}
newTitle, err := NewStringItem(titleText, font, colors.White)
if err == nil {
newTitle.Scale(titleScale)
p.title = newTitle
p.title.X = p.ScreenCenterX - p.title.W/2
p.title.Y = p.barY + p.barH + 40
}
return nil
}
// Update percentage texture when it changes
percent := 0
if p.TotalBytes.Load() > 0 {
percent = int(p.BytesWritten.Load() * 100 / p.TotalBytes.Load())
if percent > 100 {
percent = 100
}
}
if percent != p.lastPercent {
p.lastPercent = percent
fm := fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return nil
}
gl.DeleteTextures(1, &p.percentText.ID)
baseFontSizeStr, _ := db.GetConfigValue("init_font_size")
baseFontSize, _ := strconv.Atoi(baseFontSizeStr)
percentScale := float32(32) / float32(baseFontSize)
newText, err := NewStringItem(fmt.Sprintf("%d%%", percent), font, colors.White)
if err == nil {
newText.Scale(percentScale)
p.percentText = newText
}
}
return nil
}
func (p *DownloadProgressPage) HandleEvent(event *sdl.Event) error {
// Inescapable while downloading
if p.state != downloadStateFinished {
return nil
}
switch e := (*event).(type) {
case *sdl.KeyboardEvent:
if e.Type != sdl.KEYDOWN {
return nil
}
if e.Keysym.Sym == sdl.K_RETURN {
if p.HasError.Load() || p.NextPage == nil {
p.App.SchedulePop()
} else {
p.App.SwitchPage(p.NextPage)
}
}
}
return nil
}
func (p *DownloadProgressPage) Render() error {
// Render title
p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color)
// Progress bar background (dark gray)
bgColor := sdl.Color{R: 60, G: 60, B: 60, A: 255}
barPos := mgl32.Vec2{float32(p.barX), float32(p.barY)}
barDim := mgl32.Vec2{float32(p.barW), float32(p.barH)}
p.RenderSolidColorQuad(p.solidShader, barPos, barDim, bgColor)
// Progress bar foreground
progress := float32(0)
if p.TotalBytes.Load() > 0 {
progress = float32(p.BytesWritten.Load()) / float32(p.TotalBytes.Load())
if progress > 1.0 {
progress = 1.0
}
}
if progress > 0 {
fgColor := sdl.Color{R: 255, G: 255, B: 255, A: 255}
fgDim := mgl32.Vec2{float32(p.barW) * progress, float32(p.barH)}
p.RenderSolidColorQuad(p.solidShader, barPos, fgDim, fgColor)
}
// Render percentage text below bar
p.percentText.X = p.ScreenCenterX - p.percentText.W/2
p.percentText.Y = p.barY - p.percentText.H - 20
p.RenderTexture(p.textShader, p.percentText.ID, p.percentText.Position(), p.percentText.RawDimensions(), p.percentText.Color)
// Show prompt when finished
if p.state == downloadStateFinished {
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
p.prompt.Y = p.barY - p.percentText.H - 20 - p.prompt.H - 20
p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color)
}
return nil
}
func (p *DownloadProgressPage) Destroy() error {
keybinds.Enabled = true
gl.DeleteTextures(1, &p.title.ID)
gl.DeleteTextures(1, &p.prompt.ID)
gl.DeleteTextures(1, &p.percentText.ID)
return nil
}
+395
View File
@@ -0,0 +1,395 @@
package page
import (
"fmt"
"regexp"
"strconv"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
// emailRegex validates email format
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
type EmailAddressPage struct {
Base
title string
titleTextureID uint32
titleTextureWidth int32
titleTextureHeight int32
currentEmailDisplayTextureID uint32
currentEmailDisplayTextureWidth int32
currentEmailDisplayTextureHeight int32
inputText string
inputTextTextureID uint32
inputTextTextureWidth int32
inputTextTextureHeight int32
// Error message handling
errorMessage string
errorTextureID uint32
errorTextureWidth int32
errorTextureHeight int32
// isFirstTimeSetup tracks whether page was loaded empty (first-time setup)
isFirstTimeSetup bool
}
// isValidEmail checks if the given string is a valid email format
func isValidEmail(email string) bool {
return emailRegex.MatchString(email)
}
// Init performs basic page initialization and creation of static assets.
func (p *EmailAddressPage) Init(app ApplicationInterface) error {
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
p.title = "Your Email Address"
// Get application font from database
applicationFont, err := db.GetConfigValue("application_font")
if err != nil {
return fmt.Errorf("retrieving application_font from db: %w", err)
}
fm := fontManager.Get()
textShader, ok := shaderManager.Get().Get("text")
if !ok {
return fmt.Errorf("unable to fetch text shader")
}
// Create title texture
titleTextureID, titleWidth, titleHeight, err := fm.CreateStringTexture(applicationFont, p.title, textShader)
if err != nil {
return fmt.Errorf("failed to create title texture: %w", err)
}
if titleTextureID == 0 {
return fmt.Errorf("CreateStringTexture returned a nil texture ID")
}
p.titleTextureID = titleTextureID
p.titleTextureWidth = titleWidth
p.titleTextureHeight = titleHeight
// Load existing email address from database
emailAddress, err := db.GetConfigValue("email_address")
if err != nil {
return fmt.Errorf("retrieving email_address from db: %w", err)
}
if emailAddress != "" {
p.inputText = emailAddress
p.isFirstTimeSetup = false
// Create current email display texture
currentEmailText := fmt.Sprintf("Current Email: %s", emailAddress)
currentEmailTextureID, currentEmailWidth, currentEmailHeight, err := fm.CreateStringTexture(applicationFont, currentEmailText, textShader)
if err != nil {
return fmt.Errorf("failed to create current email display texture: %w", err)
}
if currentEmailTextureID == 0 {
return fmt.Errorf("CreateStringTexture returned a nil texture ID for current email display")
}
p.currentEmailDisplayTextureID = currentEmailTextureID
p.currentEmailDisplayTextureWidth = currentEmailWidth
p.currentEmailDisplayTextureHeight = currentEmailHeight
} else {
p.inputText = ""
p.isFirstTimeSetup = true
}
sdl.StartTextInput()
return nil
}
// HandleEvent processes SDL events and handles them appropriately for the EmailAddressPage.
func (p *EmailAddressPage) HandleEvent(event *sdl.Event) error {
actualEvent := *event
if !sdl.IsTextInputActive() {
sdl.StartTextInput()
}
switch e := actualEvent.(type) {
case *sdl.TextInputEvent:
p.inputText += e.GetText()
// Clear error when user starts typing
p.errorMessage = ""
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
switch e.Keysym.Sym {
case sdl.K_ESCAPE:
// Block escape during first-time setup
if p.isFirstTimeSetup {
return nil
}
// Allow escape back to settings if coming from settings
sdl.StopTextInput()
p.App.UnwindToPage(&Settings{})
case sdl.K_RETURN:
// Validate email - check empty first, then format
if p.inputText == "" {
p.errorMessage = "Email address cannot be empty"
return nil
}
if !isValidEmail(p.inputText) {
p.errorMessage = "Please enter a valid email address"
return nil
}
sdl.StopTextInput()
// Save email address to database
if err := db.SetConfigValue("email_address", p.inputText); err != nil {
return fmt.Errorf("failed to save email address: %w", err)
}
if p.isFirstTimeSetup {
// First-time setup: navigate to License Key page
p.App.PushPage(&LicenseKeyPage{})
} else {
// Coming from settings: rewind back to settings
p.App.UnwindToPage(&Settings{})
}
case sdl.K_BACKSPACE:
if len(p.inputText) > 0 {
p.inputText = p.inputText[:len(p.inputText)-1]
// Clear error when user edits input
p.errorMessage = ""
}
}
}
}
return nil
}
// Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32.
func (p *EmailAddressPage) Update(dt float32) error {
// Get application font from database
applicationFont, err := db.GetConfigValue("application_font")
if err != nil {
return fmt.Errorf("retrieving application_font from db: %w", err)
}
fm := fontManager.Get()
sm := shaderManager.Get()
shader, ok := sm.Get("text")
if !ok {
return fmt.Errorf("unable to fetch text shader")
}
// Update input text texture
if len(p.inputText) > 0 {
// Create the new inputText texture
inputTextTextureID, inputTextTextureWidth, inputTextTextureHeight, err := fm.CreateStringTexture(
applicationFont,
p.inputText,
shader,
)
if err != nil {
return fmt.Errorf("unable to create new input text texture: %w", err)
}
// Delete old texture if it exists
if p.inputTextTextureID != 0 {
gl.DeleteTextures(1, &p.inputTextTextureID)
}
p.inputTextTextureID = inputTextTextureID
p.inputTextTextureWidth = inputTextTextureWidth
p.inputTextTextureHeight = inputTextTextureHeight
}
// Update error message texture
if p.errorMessage != "" {
errorTextureID, errorWidth, errorHeight, err := fm.CreateStringTexture(
applicationFont,
p.errorMessage,
shader,
)
if err != nil {
return fmt.Errorf("unable to create error text texture: %w", err)
}
// Delete old error texture if it exists
if p.errorTextureID != 0 {
gl.DeleteTextures(1, &p.errorTextureID)
}
p.errorTextureID = errorTextureID
p.errorTextureWidth = errorWidth
p.errorTextureHeight = errorHeight
} else {
// Clear error texture if no error message
if p.errorTextureID != 0 {
gl.DeleteTextures(1, &p.errorTextureID)
p.errorTextureID = 0
}
}
return nil
}
// Render performs page drawing logic.
func (p *EmailAddressPage) Render() error {
// Get base font size from database
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
sm := shaderManager.Get()
// 1. Calculate Scale
titleFontSize := int32(64)
titleScale := float32(titleFontSize) / float32(baseFontSize)
currentEmailFontSize := int32(28)
currentEmailScale := float32(currentEmailFontSize) / float32(baseFontSize)
padding := int32(4)
verticalMargin := int32(20)
// 2. Define dimensions
targetFontSize := int32(36)
scale := float32(targetFontSize) / float32(baseFontSize)
border := int32(3)
boxH := targetFontSize + (padding * 2)
boxW := int32(600) + (padding * 2)
boxDimensions := mgl32.Vec2{float32(boxW), float32(boxH)}
borderBoxH := boxH + (border * 2)
borderBoxW := boxW + (border * 2)
borderBoxDimensions := mgl32.Vec2{float32(borderBoxW), float32(borderBoxH)}
titleH := float32(p.titleTextureHeight) * titleScale
titleW := float32(p.titleTextureWidth) * titleScale
titleDimensions := mgl32.Vec2{titleW, titleH}
inputTextH := float32(p.inputTextTextureHeight) * scale
inputTextW := float32(p.inputTextTextureWidth) * scale
inputTextDimensions := mgl32.Vec2{inputTextW, inputTextH}
var currentEmailDisplayH, currentEmailDisplayW float32
var currentEmailDisplayDimensions mgl32.Vec2
if p.currentEmailDisplayTextureID != 0 {
currentEmailDisplayH = float32(p.currentEmailDisplayTextureHeight) * currentEmailScale
currentEmailDisplayW = float32(p.currentEmailDisplayTextureWidth) * currentEmailScale
currentEmailDisplayDimensions = mgl32.Vec2{currentEmailDisplayW, currentEmailDisplayH}
}
// Error text dimensions
errorFontSize := int32(20)
errorScale := float32(errorFontSize) / float32(baseFontSize)
var errorTextH, errorTextW float32
var errorTextDimensions mgl32.Vec2
if p.errorTextureID != 0 {
errorTextH = float32(p.errorTextureHeight) * errorScale
errorTextW = float32(p.errorTextureWidth) * errorScale
errorTextDimensions = mgl32.Vec2{errorTextW, errorTextH}
}
// 3. Define positions
borderBoxY := p.Base.ScreenCenterY - (borderBoxH / 2)
borderBoxX := p.Base.ScreenCenterX - (borderBoxW / 2)
borderBoxPosition := mgl32.Vec2{float32(borderBoxX), float32(borderBoxY)}
boxY := p.Base.ScreenCenterY - (boxH / 2)
boxX := p.Base.ScreenCenterX - (boxW / 2)
boxPosition := mgl32.Vec2{float32(boxX), float32(boxY)}
titleY := borderBoxY + borderBoxH + verticalMargin
titleX := p.Base.ScreenCenterX - int32(titleW/2)
titlePosition := mgl32.Vec2{float32(titleX), float32(titleY)}
inputTextY := boxY + padding
inputTextX := boxX + padding
inputTextPosition := mgl32.Vec2{float32(inputTextX), float32(inputTextY)}
var currentEmailDisplayPosition mgl32.Vec2
if p.currentEmailDisplayTextureID != 0 {
currentEmailDisplayY := borderBoxY - int32(currentEmailDisplayH) - verticalMargin
currentEmailDisplayX := p.Base.ScreenCenterX - int32(currentEmailDisplayW/2)
currentEmailDisplayPosition = mgl32.Vec2{float32(currentEmailDisplayX), float32(currentEmailDisplayY)}
}
// Error text position (below current email display to avoid overlap)
var errorTextPosition mgl32.Vec2
if p.errorTextureID != 0 {
var errorTextY int32
if p.currentEmailDisplayTextureID != 0 {
// Position below the current email display
errorTextY = borderBoxY - int32(currentEmailDisplayH) - verticalMargin - int32(errorTextH) - (verticalMargin / 2)
} else {
// No current email display, position below the input box
errorTextY = borderBoxY - int32(errorTextH) - (verticalMargin / 2)
}
errorTextX := p.Base.ScreenCenterX - int32(errorTextW/2)
errorTextPosition = mgl32.Vec2{float32(errorTextX), float32(errorTextY)}
}
// 4. Define Colors
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
grey := sdl.Color{R: 120, G: 120, B: 120, A: 255}
darkGrey := sdl.Color{R: 30, G: 30, B: 30, A: 255}
// 5. Get Shaders
textShader, _ := sm.Get("text")
solidShader, _ := sm.Get("solid_color")
// 6. Draw everything
p.Base.RenderSolidColorQuad(solidShader, borderBoxPosition, borderBoxDimensions, grey)
p.Base.RenderSolidColorQuad(solidShader, boxPosition, boxDimensions, darkGrey)
p.Base.RenderTexture(textShader, p.titleTextureID, titlePosition, titleDimensions, white)
// Draw current email display if it exists
if p.currentEmailDisplayTextureID != 0 {
p.Base.RenderTexture(textShader, p.currentEmailDisplayTextureID, currentEmailDisplayPosition, currentEmailDisplayDimensions, white)
}
if len(p.inputText) > 0 {
p.Base.RenderTexture(textShader, p.inputTextTextureID, inputTextPosition, inputTextDimensions, white)
}
// Draw error message if it exists
if p.errorTextureID != 0 {
p.Base.RenderTexture(textShader, p.errorTextureID, errorTextPosition, errorTextDimensions, colors.ErrorRed)
}
return nil
}
// Destroy cleans up page-specific resources.
func (p *EmailAddressPage) Destroy() error {
gl.DeleteTextures(1, &p.titleTextureID)
// Delete current email display texture if it exists
if p.currentEmailDisplayTextureID != 0 {
gl.DeleteTextures(1, &p.currentEmailDisplayTextureID)
p.currentEmailDisplayTextureID = 0
}
// Delete input text texture if it exists
if p.inputTextTextureID != 0 {
gl.DeleteTextures(1, &p.inputTextTextureID)
p.inputTextTextureID = 0
}
// Delete error texture if it exists
if p.errorTextureID != 0 {
gl.DeleteTextures(1, &p.errorTextureID)
p.errorTextureID = 0
}
sdl.StopTextInput()
return p.Base.Destroy()
}
+458
View File
@@ -0,0 +1,458 @@
package page
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/logger"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type LicenseKeyPage struct {
Base
title string
titleTextureID uint32
titleTextureWidth int32
titleTextureHeight int32
currentLicenseDisplayTextureID uint32
currentLicenseDisplayTextureWidth int32
currentLicenseDisplayTextureHeight int32
inputText string
inputTextTextureID uint32
inputTextTextureWidth int32
inputTextTextureHeight int32
// Error message handling
errorMessage string
errorTextureID uint32
errorTextureWidth int32
errorTextureHeight int32
// isFirstTimeSetup tracks whether page was loaded empty (first-time setup)
isFirstTimeSetup bool
}
// isValidLicenseKey checks if the license key has exactly 16 alphanumeric characters
func isValidLicenseKey(key string) bool {
cleaned := strings.ReplaceAll(key, "-", "")
if len(cleaned) != 16 {
return false
}
for _, r := range cleaned {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) {
return false
}
}
return true
}
// Init performs basic page initialization and creation of static assets.
func (p *LicenseKeyPage) Init(app ApplicationInterface) error {
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
p.title = "License Key"
// Get application font from database
applicationFont, err := db.GetConfigValue("application_font")
if err != nil {
return fmt.Errorf("retrieving application_font from db: %w", err)
}
fm := fontManager.Get()
textShader, ok := shaderManager.Get().Get("text")
if !ok {
return fmt.Errorf("unable to fetch text shader")
}
// Create title texture
titleTextureID, titleWidth, titleHeight, err := fm.CreateStringTexture(applicationFont, p.title, textShader)
if err != nil {
return fmt.Errorf("failed to create title texture: %w", err)
}
if titleTextureID == 0 {
return fmt.Errorf("CreateStringTexture returned a nil texture ID")
}
p.titleTextureID = titleTextureID
p.titleTextureWidth = titleWidth
p.titleTextureHeight = titleHeight
// Load existing license key from database
licenseKey, err := db.GetConfigValue("license_key")
if err != nil {
return fmt.Errorf("retrieving license_key from db: %w", err)
}
if licenseKey != "" {
p.inputText = licenseKey
p.isFirstTimeSetup = false
// Create current license key display texture
currentLicenseText := fmt.Sprintf("Current License Key: %s", licenseKey)
currentLicenseTextureID, currentLicenseWidth, currentLicenseHeight, err := fm.CreateStringTexture(applicationFont, currentLicenseText, textShader)
if err != nil {
return fmt.Errorf("failed to create current license display texture: %w", err)
}
if currentLicenseTextureID == 0 {
return fmt.Errorf("CreateStringTexture returned a nil texture ID for current license display")
}
p.currentLicenseDisplayTextureID = currentLicenseTextureID
p.currentLicenseDisplayTextureWidth = currentLicenseWidth
p.currentLicenseDisplayTextureHeight = currentLicenseHeight
} else {
p.inputText = ""
p.isFirstTimeSetup = true
}
sdl.StartTextInput()
return nil
}
// formatLicenseKey formats the input string with dashes every 4 characters
// and limits to 16 alphanumeric characters (not counting dashes)
func formatLicenseKey(input string) string {
// Remove all dashes and non-alphanumeric characters
cleaned := strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
return r
}
return -1
}, input)
// Limit to 16 characters
if len(cleaned) > 16 {
cleaned = cleaned[:16]
}
// Insert dashes every 4 characters
var result strings.Builder
for i, char := range cleaned {
if i > 0 && i%4 == 0 {
result.WriteRune('-')
}
result.WriteRune(char)
}
return result.String()
}
// HandleEvent processes SDL events and handles them appropriately for the LicenseKeyPage.
func (p *LicenseKeyPage) HandleEvent(event *sdl.Event) error {
actualEvent := *event
if !sdl.IsTextInputActive() {
sdl.StartTextInput()
}
switch e := actualEvent.(type) {
case *sdl.TextInputEvent:
// Add the new character and reformat
p.inputText += e.GetText()
p.inputText = formatLicenseKey(p.inputText)
// Clear error when user starts typing
p.errorMessage = ""
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
switch e.Keysym.Sym {
case sdl.K_ESCAPE:
// Block escape during first-time setup
if p.isFirstTimeSetup {
return nil
}
// Allow escape back to settings if coming from settings
sdl.StopTextInput()
p.App.UnwindToPage(&Settings{})
case sdl.K_RETURN:
// Validate license key - check empty first, then length, then characters
cleaned := strings.ReplaceAll(p.inputText, "-", "")
if cleaned == "" {
p.errorMessage = "License key cannot be empty"
return nil
}
if len(cleaned) != 16 {
p.errorMessage = fmt.Sprintf("License key must be 16 characters (currently %d)", len(cleaned))
return nil
}
// Check for invalid characters
for _, r := range cleaned {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) {
p.errorMessage = "License key must contain only letters and numbers"
return nil
}
}
sdl.StopTextInput()
// Save license key to database
if err := db.SetConfigValue("license_key", p.inputText); err != nil {
return fmt.Errorf("failed to save license key: %w", err)
}
// Write license key to plain text file for the updater
if homeDir, err := os.UserHomeDir(); err == nil {
keyPath := filepath.Join(homeDir, ".local", "share", "rhemawave", "license_key")
if err := os.WriteFile(keyPath, []byte(p.inputText), 0644); err != nil {
logger.Error("Failed to write license key file: %v", err)
}
}
if p.isFirstTimeSetup {
// First-time setup: rewind to Welcome page
p.App.UnwindToPage(&Welcome{})
} else {
// Coming from settings: rewind back to settings
p.App.UnwindToPage(&Settings{})
}
case sdl.K_BACKSPACE:
if len(p.inputText) > 0 {
// Remove last character and reformat
// Strip all dashes first to get raw characters
cleaned := strings.ReplaceAll(p.inputText, "-", "")
if len(cleaned) > 0 {
cleaned = cleaned[:len(cleaned)-1]
}
p.inputText = formatLicenseKey(cleaned)
// Clear error when user edits input
p.errorMessage = ""
}
}
}
}
return nil
}
// Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32.
func (p *LicenseKeyPage) Update(dt float32) error {
// Get application font from database
applicationFont, err := db.GetConfigValue("application_font")
if err != nil {
return fmt.Errorf("retrieving application_font from db: %w", err)
}
fm := fontManager.Get()
sm := shaderManager.Get()
shader, ok := sm.Get("text")
if !ok {
return fmt.Errorf("unable to fetch text shader")
}
// Update input text texture
if len(p.inputText) > 0 {
// Create the new inputText texture
inputTextTextureID, inputTextTextureWidth, inputTextTextureHeight, err := fm.CreateStringTexture(
applicationFont,
p.inputText,
shader,
)
if err != nil {
return fmt.Errorf("unable to create new input text texture: %w", err)
}
// Delete old texture if it exists
if p.inputTextTextureID != 0 {
gl.DeleteTextures(1, &p.inputTextTextureID)
}
p.inputTextTextureID = inputTextTextureID
p.inputTextTextureWidth = inputTextTextureWidth
p.inputTextTextureHeight = inputTextTextureHeight
}
// Update error message texture
if p.errorMessage != "" {
errorTextureID, errorWidth, errorHeight, err := fm.CreateStringTexture(
applicationFont,
p.errorMessage,
shader,
)
if err != nil {
return fmt.Errorf("unable to create error text texture: %w", err)
}
// Delete old error texture if it exists
if p.errorTextureID != 0 {
gl.DeleteTextures(1, &p.errorTextureID)
}
p.errorTextureID = errorTextureID
p.errorTextureWidth = errorWidth
p.errorTextureHeight = errorHeight
} else {
// Clear error texture if no error message
if p.errorTextureID != 0 {
gl.DeleteTextures(1, &p.errorTextureID)
p.errorTextureID = 0
}
}
return nil
}
// Render performs page drawing logic.
func (p *LicenseKeyPage) Render() error {
// Get base font size from database
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
sm := shaderManager.Get()
// 1. Calculate Scale
titleFontSize := int32(64)
titleScale := float32(titleFontSize) / float32(baseFontSize)
currentLicenseFontSize := int32(28)
currentLicenseScale := float32(currentLicenseFontSize) / float32(baseFontSize)
padding := int32(4)
verticalMargin := int32(20)
// 2. Define dimensions
targetFontSize := int32(36)
scale := float32(targetFontSize) / float32(baseFontSize)
border := int32(3)
boxH := targetFontSize + (padding * 2)
boxW := int32(600) + (padding * 2)
boxDimensions := mgl32.Vec2{float32(boxW), float32(boxH)}
borderBoxH := boxH + (border * 2)
borderBoxW := boxW + (border * 2)
borderBoxDimensions := mgl32.Vec2{float32(borderBoxW), float32(borderBoxH)}
titleH := float32(p.titleTextureHeight) * titleScale
titleW := float32(p.titleTextureWidth) * titleScale
titleDimensions := mgl32.Vec2{titleW, titleH}
inputTextH := float32(p.inputTextTextureHeight) * scale
inputTextW := float32(p.inputTextTextureWidth) * scale
inputTextDimensions := mgl32.Vec2{inputTextW, inputTextH}
var currentLicenseDisplayH, currentLicenseDisplayW float32
var currentLicenseDisplayDimensions mgl32.Vec2
if p.currentLicenseDisplayTextureID != 0 {
currentLicenseDisplayH = float32(p.currentLicenseDisplayTextureHeight) * currentLicenseScale
currentLicenseDisplayW = float32(p.currentLicenseDisplayTextureWidth) * currentLicenseScale
currentLicenseDisplayDimensions = mgl32.Vec2{currentLicenseDisplayW, currentLicenseDisplayH}
}
// Error text dimensions
errorFontSize := int32(20)
errorScale := float32(errorFontSize) / float32(baseFontSize)
var errorTextH, errorTextW float32
var errorTextDimensions mgl32.Vec2
if p.errorTextureID != 0 {
errorTextH = float32(p.errorTextureHeight) * errorScale
errorTextW = float32(p.errorTextureWidth) * errorScale
errorTextDimensions = mgl32.Vec2{errorTextW, errorTextH}
}
// 3. Define positions
borderBoxY := p.Base.ScreenCenterY - (borderBoxH / 2)
borderBoxX := p.Base.ScreenCenterX - (borderBoxW / 2)
borderBoxPosition := mgl32.Vec2{float32(borderBoxX), float32(borderBoxY)}
boxY := p.Base.ScreenCenterY - (boxH / 2)
boxX := p.Base.ScreenCenterX - (boxW / 2)
boxPosition := mgl32.Vec2{float32(boxX), float32(boxY)}
titleY := borderBoxY + borderBoxH + verticalMargin
titleX := p.Base.ScreenCenterX - int32(titleW/2)
titlePosition := mgl32.Vec2{float32(titleX), float32(titleY)}
inputTextY := boxY + padding
inputTextX := boxX + padding
inputTextPosition := mgl32.Vec2{float32(inputTextX), float32(inputTextY)}
var currentLicenseDisplayPosition mgl32.Vec2
if p.currentLicenseDisplayTextureID != 0 {
currentLicenseDisplayY := borderBoxY - int32(currentLicenseDisplayH) - verticalMargin
currentLicenseDisplayX := p.Base.ScreenCenterX - int32(currentLicenseDisplayW/2)
currentLicenseDisplayPosition = mgl32.Vec2{float32(currentLicenseDisplayX), float32(currentLicenseDisplayY)}
}
// Error text position (below current license display to avoid overlap)
var errorTextPosition mgl32.Vec2
if p.errorTextureID != 0 {
var errorTextY int32
if p.currentLicenseDisplayTextureID != 0 {
// Position below the current license display
errorTextY = borderBoxY - int32(currentLicenseDisplayH) - verticalMargin - int32(errorTextH) - (verticalMargin / 2)
} else {
// No current license display, position below the input box
errorTextY = borderBoxY - int32(errorTextH) - (verticalMargin / 2)
}
errorTextX := p.Base.ScreenCenterX - int32(errorTextW/2)
errorTextPosition = mgl32.Vec2{float32(errorTextX), float32(errorTextY)}
}
// 4. Get Shaders
textShader, _ := sm.Get("text")
solidShader, _ := sm.Get("solid_color")
// 5. Draw everything
p.Base.RenderSolidColorQuad(solidShader, borderBoxPosition, borderBoxDimensions, colors.Gray)
p.Base.RenderSolidColorQuad(solidShader, boxPosition, boxDimensions, colors.DarkGray)
p.Base.RenderTexture(textShader, p.titleTextureID, titlePosition, titleDimensions, colors.White)
// Draw current license display if it exists
if p.currentLicenseDisplayTextureID != 0 {
p.Base.RenderTexture(textShader, p.currentLicenseDisplayTextureID, currentLicenseDisplayPosition, currentLicenseDisplayDimensions, colors.White)
}
if len(p.inputText) > 0 {
p.Base.RenderTexture(textShader, p.inputTextTextureID, inputTextPosition, inputTextDimensions, colors.White)
}
// Draw error message if it exists
if p.errorTextureID != 0 {
p.Base.RenderTexture(textShader, p.errorTextureID, errorTextPosition, errorTextDimensions, colors.ErrorRed)
}
return nil
}
// Destroy cleans up page-specific resources.
func (p *LicenseKeyPage) Destroy() error {
gl.DeleteTextures(1, &p.titleTextureID)
// Delete current license display texture if it exists
if p.currentLicenseDisplayTextureID != 0 {
gl.DeleteTextures(1, &p.currentLicenseDisplayTextureID)
p.currentLicenseDisplayTextureID = 0
}
// Delete input text texture if it exists
if p.inputTextTextureID != 0 {
gl.DeleteTextures(1, &p.inputTextTextureID)
p.inputTextTextureID = 0
}
// Delete error texture if it exists
if p.errorTextureID != 0 {
gl.DeleteTextures(1, &p.errorTextureID)
p.errorTextureID = 0
}
sdl.StopTextInput()
return p.Base.Destroy()
}
+251
View File
@@ -0,0 +1,251 @@
package page
import (
"fmt"
"strconv"
"strings"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type LogViewerPage struct {
Base
titleTextureID uint32
titleTextureWidth int32
titleTextureHeight int32
lines []string
lineTextureIDs []uint32
lineTextureWidths []int32
lineTextureHeights []int32
hintTextureID uint32
hintTextureWidth int32
hintTextureHeight int32
scrollOffset int32
maxScrollOffset int32
lineHeight int32
visibleHeight int32
}
func (p *LogViewerPage) Init(app ApplicationInterface) error {
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
logs, err := db.GetLogs(1000)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
// Format and word-wrap each log entry
for _, entry := range logs {
formatted := fmt.Sprintf("[%s] [%s] %s", entry.Timestamp, entry.Level, entry.Message)
wrapped := wrapText(formatted, 100)
p.lines = append(p.lines, strings.Split(wrapped, "\n")...)
}
applicationFont, err := db.GetConfigValue("application_font")
if err != nil {
return fmt.Errorf("retrieving application_font from db: %w", err)
}
fm := fontManager.Get()
textShader, ok := shaderManager.Get().Get("text")
if !ok {
return fmt.Errorf("unable to fetch text shader")
}
// Title texture
titleID, titleW, titleH, err := fm.CreateStringTexture(applicationFont, "Application Logs", textShader)
if err != nil {
return fmt.Errorf("failed to create title texture: %w", err)
}
p.titleTextureID = titleID
p.titleTextureWidth = titleW
p.titleTextureHeight = titleH
// Line textures
p.lineTextureIDs = make([]uint32, len(p.lines))
p.lineTextureWidths = make([]int32, len(p.lines))
p.lineTextureHeights = make([]int32, len(p.lines))
for i, line := range p.lines {
if strings.TrimSpace(line) == "" {
continue
}
texID, w, h, err := fm.CreateStringTexture(applicationFont, line, textShader)
if err != nil {
return fmt.Errorf("failed to create line texture: %w", err)
}
p.lineTextureIDs[i] = texID
p.lineTextureWidths[i] = w
p.lineTextureHeights[i] = h
}
// Hint texture
hintText := "Arrow Keys / PgUp/PgDn to scroll | F1|2|3 to Exit"
hintID, hintW, hintH, err := fm.CreateStringTexture(applicationFont, hintText, textShader)
if err != nil {
return fmt.Errorf("failed to create hint texture: %w", err)
}
p.hintTextureID = hintID
p.hintTextureWidth = hintW
p.hintTextureHeight = hintH
return nil
}
func (p *LogViewerPage) HandleEvent(event *sdl.Event) error {
switch e := (*event).(type) {
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
switch e.Keysym.Sym {
case sdl.K_BACKSPACE:
p.App.SchedulePop()
case sdl.K_UP:
p.scrollOffset -= p.lineHeight
if p.scrollOffset < 0 {
p.scrollOffset = 0
}
case sdl.K_DOWN:
p.scrollOffset += p.lineHeight
if p.scrollOffset > p.maxScrollOffset {
p.scrollOffset = p.maxScrollOffset
}
case sdl.K_PAGEUP:
p.scrollOffset -= p.visibleHeight
if p.scrollOffset < 0 {
p.scrollOffset = 0
}
case sdl.K_PAGEDOWN:
p.scrollOffset += p.visibleHeight
if p.scrollOffset > p.maxScrollOffset {
p.scrollOffset = p.maxScrollOffset
}
case sdl.K_HOME:
p.scrollOffset = 0
case sdl.K_END:
p.scrollOffset = p.maxScrollOffset
}
}
case *sdl.MouseWheelEvent:
p.scrollOffset -= e.Y * p.lineHeight * 3
if p.scrollOffset < 0 {
p.scrollOffset = 0
}
if p.scrollOffset > p.maxScrollOffset {
p.scrollOffset = p.maxScrollOffset
}
}
return nil
}
func (p *LogViewerPage) Update(dt float32) error {
return nil
}
func (p *LogViewerPage) Render() error {
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
sm := shaderManager.Get()
textShader, _ := sm.Get("text")
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
grey := sdl.Color{R: 180, G: 180, B: 180, A: 255}
titleFontSize := int32(48)
titleScale := float32(titleFontSize) / float32(baseFontSize)
contentFontSize := int32(24)
contentScale := float32(contentFontSize) / float32(baseFontSize)
hintFontSize := int32(20)
hintScale := float32(hintFontSize) / float32(baseFontSize)
margin := int32(40)
lineSpacing := int32(4)
p.lineHeight = contentFontSize + lineSpacing
contentColumnWidth := int32(float32(p.Base.ScreenWidth) * 0.55)
contentLeftX := p.Base.ScreenCenterX - contentColumnWidth/2
// Title
titleH := float32(p.titleTextureHeight) * titleScale
titleW := float32(p.titleTextureWidth) * titleScale
titleX := p.Base.ScreenCenterX - int32(titleW/2)
titleY := p.Base.ScreenHeight - margin - int32(titleH)
p.Base.RenderTexture(textShader, p.titleTextureID, mgl32.Vec2{float32(titleX), float32(titleY)}, mgl32.Vec2{titleW, titleH}, white)
// Hint at bottom
hintH := float32(p.hintTextureHeight) * hintScale
hintW := float32(p.hintTextureWidth) * hintScale
hintX := p.Base.ScreenCenterX - int32(hintW/2)
hintY := margin
p.Base.RenderTexture(textShader, p.hintTextureID, mgl32.Vec2{float32(hintX), float32(hintY)}, mgl32.Vec2{hintW, hintH}, grey)
// Content area bounds
contentTop := titleY - margin
contentBottom := hintY + int32(hintH) + margin
p.visibleHeight = contentTop - contentBottom
// Total content height
totalContentHeight := int32(len(p.lines)) * p.lineHeight
p.maxScrollOffset = totalContentHeight - p.visibleHeight
if p.maxScrollOffset < 0 {
p.maxScrollOffset = 0
}
// Render visible lines
currentY := contentTop - p.lineHeight + p.scrollOffset
for i, line := range p.lines {
if strings.TrimSpace(line) == "" {
currentY -= p.lineHeight
continue
}
if p.lineTextureIDs[i] == 0 {
currentY -= p.lineHeight
continue
}
if currentY > contentTop || currentY+p.lineHeight < contentBottom {
currentY -= p.lineHeight
continue
}
lineW := float32(p.lineTextureWidths[i]) * contentScale
lineH := float32(p.lineTextureHeights[i]) * contentScale
p.Base.RenderTexture(textShader, p.lineTextureIDs[i], mgl32.Vec2{float32(contentLeftX), float32(currentY)}, mgl32.Vec2{lineW, lineH}, white)
currentY -= p.lineHeight
}
return nil
}
func (p *LogViewerPage) Destroy() error {
gl.DeleteTextures(1, &p.titleTextureID)
gl.DeleteTextures(1, &p.hintTextureID)
for _, texID := range p.lineTextureIDs {
if texID != 0 {
gl.DeleteTextures(1, &texID)
}
}
return p.Base.Destroy()
}
+198
View File
@@ -0,0 +1,198 @@
// Package page provides the interface for pages
package page
import (
"fmt"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
// Page defines the contract for all application screens.
type Page interface {
Init(app ApplicationInterface) error
HandleEvent(event *sdl.Event) error
Update(dt float32) error
Render() error
// Destroy cleans up the page's resources. The Application's page manager
// is responsible for calling this method
Destroy() error
}
// ApplicationInterface defines the set of methods that a page can use to
// interact with the main application.
type ApplicationInterface interface {
SwitchPage(p Page)
PushPage(p Page)
UnwindToPage(p Page)
GetDrawableSize() (int32, int32)
SchedulePop()
Quit()
}
// Base is a helper struct that pages can embed to get common functionality.
type Base struct {
App ApplicationInterface
QuadVAO uint32
QuadVBO uint32
Projection mgl32.Mat4
ScreenWidth int32
ScreenHeight int32
ScreenCenterX int32
ScreenCenterY int32
}
// Init stores the application context and sets up common resources.
func (p *Base) Init(app ApplicationInterface) error {
p.App = app
vertices := []float32{
// pos(x,y) tex(u,v)
0.0, 1.0, 0.0, 1.0,
1.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 1.0,
1.0, 1.0, 1.0, 1.0,
1.0, 0.0, 1.0, 0.0,
}
screenWidth, screenHeight := p.App.GetDrawableSize()
p.Projection = mgl32.Ortho(0, float32(screenWidth), 0, float32(screenHeight), -1, 1)
p.ScreenHeight = screenHeight
p.ScreenWidth = screenWidth
p.ScreenCenterX = screenWidth / 2
p.ScreenCenterY = screenHeight / 2
gl.GenVertexArrays(1, &p.QuadVAO)
gl.GenBuffers(1, &p.QuadVBO)
gl.BindVertexArray(p.QuadVAO)
gl.BindBuffer(gl.ARRAY_BUFFER, p.QuadVBO)
gl.BufferData(gl.ARRAY_BUFFER, len(vertices)*4, gl.Ptr(vertices), gl.STATIC_DRAW)
stride := int32(4 * 4) // Each vertex has 4 floats (pos.x, pos.y, tex.u, tex.v)
// Attribute 0: Vertex Position (vec2)
gl.EnableVertexAttribArray(0)
gl.VertexAttribPointerWithOffset(0, 2, gl.FLOAT, false, stride, 0)
// Attribute 1: Texture Coordinate (vec2)
gl.EnableVertexAttribArray(1)
gl.VertexAttribPointerWithOffset(1, 2, gl.FLOAT, false, stride, 2*4) // Offset by 2 floats
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
gl.BindVertexArray(0)
gl.Disable(gl.BLEND)
gl.UseProgram(0)
return nil
}
// Destroy cleans up the common OpenGL resources.
func (p *Base) Destroy() error {
gl.DeleteBuffers(1, &p.QuadVBO)
gl.DeleteVertexArrays(1, &p.QuadVAO)
return nil
}
// BeginTextureRendering sets up shared GL state for a batch of RenderTexture calls.
// Call this once before a series of RenderTexture calls, then EndTextureRendering after.
func (p *Base) BeginTextureRendering(shader *shaderManager.Shader) {
gl.UseProgram(shader.ProgramID)
gl.Uniform1i(shader.LocTexture, 0)
gl.ActiveTexture(gl.TEXTURE0)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BindVertexArray(p.QuadVAO)
}
// EndTextureRendering restores GL state after a batch of RenderTexture calls.
func (p *Base) EndTextureRendering() {
gl.BindVertexArray(0)
gl.Disable(gl.BLEND)
gl.UseProgram(0)
}
// RenderTexture draws a given texture to the screen at a specified position and size.
// For best performance, wrap multiple calls with BeginTextureRendering/EndTextureRendering.
// This method also works standalone for one-off renders.
func (p *Base) RenderTexture(shader *shaderManager.Shader, textureID uint32, position, size mgl32.Vec2, color sdl.Color) error {
gl.UseProgram(shader.ProgramID)
gl.Uniform1i(shader.LocTexture, 0)
if shader.LocTextColor == -1 {
return fmt.Errorf("could not find 'u_textColor' uniform in shader program %d", shader.ProgramID)
}
gl.Uniform4f(shader.LocTextColor, float32(color.R)/255.0, float32(color.G)/255.0, float32(color.B)/255.0, float32(color.A)/255.0)
model := mgl32.Ident4()
model = model.Mul4(mgl32.Translate3D(position.X(), position.Y(), 0))
model = model.Mul4(mgl32.Scale3D(size.X(), size.Y(), 1))
mvp := p.Projection.Mul4(model)
gl.UniformMatrix4fv(shader.LocMVPMatrix, 1, false, &mvp[0])
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, textureID)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BindVertexArray(p.QuadVAO)
gl.DrawArrays(gl.TRIANGLES, 0, 6)
gl.BindVertexArray(0)
gl.Disable(gl.BLEND)
gl.UseProgram(0)
return nil
}
// RenderTextureBatched is a lightweight version of RenderTexture for use inside
// a BeginTextureRendering/EndTextureRendering block. It skips redundant state setup.
func (p *Base) RenderTextureBatched(shader *shaderManager.Shader, textureID uint32, position, size mgl32.Vec2, color sdl.Color) {
gl.Uniform4f(shader.LocTextColor, float32(color.R)/255.0, float32(color.G)/255.0, float32(color.B)/255.0, float32(color.A)/255.0)
model := mgl32.Ident4()
model = model.Mul4(mgl32.Translate3D(position.X(), position.Y(), 0))
model = model.Mul4(mgl32.Scale3D(size.X(), size.Y(), 1))
mvp := p.Projection.Mul4(model)
gl.UniformMatrix4fv(shader.LocMVPMatrix, 1, false, &mvp[0])
gl.BindTexture(gl.TEXTURE_2D, textureID)
gl.DrawArrays(gl.TRIANGLES, 0, 6)
}
// RenderSolidColorQuad draws a rectangle of a solid color to the screen.
func (p *Base) RenderSolidColorQuad(shader *shaderManager.Shader, position, size mgl32.Vec2, color sdl.Color) error {
gl.UseProgram(shader.ProgramID)
model := mgl32.Ident4()
model = model.Mul4(mgl32.Translate3D(position.X(), position.Y(), 0))
model = model.Mul4(mgl32.Scale3D(size.X(), size.Y(), 1))
mvp := p.Projection.Mul4(model)
if shader.LocMVPMatrix == -1 {
return fmt.Errorf("RenderSolidColorQuad: could not find 'u_mvpMatrix' uniform")
}
gl.UniformMatrix4fv(shader.LocMVPMatrix, 1, false, &mvp[0])
if shader.LocColor == -1 {
return fmt.Errorf("RenderSolidColorQuad: could not find 'u_color' uniform")
}
gl.Uniform4f(shader.LocColor, float32(color.R)/255.0, float32(color.G)/255.0, float32(color.B)/255.0, float32(color.A)/255.0)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.BindVertexArray(p.QuadVAO)
gl.DrawArrays(gl.TRIANGLES, 0, 6)
gl.BindVertexArray(0)
gl.Disable(gl.BLEND)
gl.UseProgram(0)
return nil
}
// Provide default implementations for the Page interface.
func (p *Base) HandleEvent(event sdl.Event) error { return nil }
func (p *Base) Update(dt float32) error { return nil }
func (p *Base) Render() error { return nil }
+83
View File
@@ -0,0 +1,83 @@
package page
import (
"fmt"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
// PageItem defines the basic features of any item on any page
// This coupling makes creating, updating and managing pages easier
type PageItem struct {
ID uint32
W int32
H int32
X int32 // Horizontal position
Y int32 // Vertical position
Color sdl.Color
// index int
}
// Scale updates the W and H of an item based on a scale factor (usually determined by base font size / desired font size)
func (i *PageItem) Scale(scale float32) {
i.W = int32(float32(i.W) * scale)
i.H = int32(float32(i.H) * scale)
}
// Dimensions returns an mgl32.Vec2 containing the W and H of an item, respectively
func (i *PageItem) Dimensions(scale float32) mgl32.Vec2 {
return mgl32.Vec2{float32(float32(i.W) * scale), float32(float32(i.H) * scale)}
}
// RawDimensions returns a mgl32.Vec2 containing the W and H of an item, without scaling
// This method will replace Dimensions in the future. Future PageItems should first call Scale(), then RawDimensions()
func (i *PageItem) RawDimensions() mgl32.Vec2 {
return mgl32.Vec2{float32(i.W), float32(i.H)}
}
// Position returns an mgl32.Vec2 contianing the X and Y positions of an item, respectively
func (i *PageItem) Position() mgl32.Vec2 {
return mgl32.Vec2{float32(i.X), float32(i.Y)}
}
// StringItem is an extended PageItem that contains a string content and string specific methods
type StringItem struct {
PageItem
content string
}
// NewStringItem creates a new StringItem with the given content, font, and color.
// It initializes the texture ID, width, and height by creating a string texture using the font
// and shader manager.
func NewStringItem(content string, font *fontManager.FontEntry, color sdl.Color) (StringItem, error) {
var err error
fm := fontManager.Get()
sm := shaderManager.Get()
shader, ok := sm.Get("text")
if !ok {
return StringItem{}, fmt.Errorf("failed to fetch shader")
}
id, width, height, err := fm.CreateStringTexture(font.Name, content, shader)
if err != nil {
return StringItem{}, err
}
newStringItem := StringItem{
PageItem: PageItem{
ID: id,
W: width,
H: height,
Color: color,
},
content: content,
}
return newStringItem, err
}
+243
View File
@@ -0,0 +1,243 @@
package page
import (
"fmt"
"strconv"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type ProclamationOptions struct {
Base
font *fontManager.FontEntry
textShader *shaderManager.Shader
solidShader *shaderManager.Shader
title StringItem
prompt StringItem
proclamations []StringItem
proclamationIDs []uint // Database IDs corresponding to each proclamation
activeMap map[int]bool // Maps display index to selected state
highlightIndex int
verticalMargin int32
}
func (p *ProclamationOptions) Init(app ApplicationInterface) error {
var err error
p.verticalMargin = int32(20)
p.activeMap = make(map[int]bool)
p.proclamationIDs = make([]uint, 0)
// Get font
fm := fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return fmt.Errorf("failed to get font: %w", sdl.GetError())
}
p.font = font
// Get shaders
sm := shaderManager.Get()
p.textShader, ok = sm.Get("text")
if !ok {
return fmt.Errorf("failed to get text shader: %w", sdl.GetError())
}
p.solidShader, ok = sm.Get("solid_color")
if !ok {
return fmt.Errorf("failed to get solid_color shader: %w", sdl.GetError())
}
// Initialize base
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("base init failed: %w", err)
}
// Get font size from database
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size %q: %w", baseFontSizeStr, err)
}
// Calculate scales
titleScale := float32(64) / float32(baseFontSize)
itemScale := float32(32) / float32(baseFontSize)
// Create title
p.title, err = NewStringItem("Select Proclamations", p.font, colors.White)
if err != nil {
return fmt.Errorf("creating title: %w", err)
}
p.title.Scale(titleScale)
// Create prompt
p.prompt, err = NewStringItem("Arrow Keys to navigate | Space to toggle | F2 to run | Enter to return", p.font, colors.White)
if err != nil {
return fmt.Errorf("creating prompt: %w", err)
}
p.prompt.Scale(itemScale)
// Load proclamations from database
proclamations, err := db.GetProclamations()
if err != nil {
return fmt.Errorf("retrieving proclamations from db: %w", err)
}
// Create UI items for each available proclamation
for _, proclamation := range proclamations {
if !proclamation.Available {
continue
}
label, err := NewStringItem(proclamation.Title, p.font, colors.White)
if err != nil {
return fmt.Errorf("creating proclamation label: %w", err)
}
label.Scale(itemScale)
p.proclamations = append(p.proclamations, label)
p.proclamationIDs = append(p.proclamationIDs, proclamation.ID)
// Set initial selection state
if proclamation.Selected {
p.activeMap[len(p.proclamations)-1] = true
}
}
p.layout()
return nil
}
func (p *ProclamationOptions) layout() {
// Position title at bottom
p.title.X = p.ScreenCenterX - p.title.W/2
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.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H
// Position proclamation items stacked upward from prompt
for i := range p.proclamations {
p.proclamations[i].X = p.ScreenCenterX - p.proclamations[i].W/2
if i == 0 {
p.proclamations[i].Y = p.prompt.Y - p.verticalMargin*2 - p.proclamations[i].H
} else {
p.proclamations[i].Y = p.proclamations[i-1].Y - p.proclamations[i].H - 10
}
}
}
func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
switch e := (*event).(type) {
case *sdl.KeyboardEvent:
if e.Type != sdl.KEYDOWN {
return nil
}
switch e.Keysym.Sym {
case sdl.K_UP:
if p.highlightIndex > 0 {
p.highlightIndex--
}
case sdl.K_DOWN:
if p.highlightIndex < len(p.proclamations)-1 {
p.highlightIndex++
}
case sdl.K_BACKSPACE:
p.App.UnwindToPage(&Settings{})
case sdl.K_SPACE:
// Toggle selection for highlighted item
if p.activeMap[p.highlightIndex] {
delete(p.activeMap, p.highlightIndex)
} else {
p.activeMap[p.highlightIndex] = true
}
// Immediately save to database
if err := p.toggleProclamation(p.highlightIndex); err != nil {
return fmt.Errorf("failed to toggle proclamation: %w", err)
}
case sdl.K_RETURN:
// Just return to settings - no saving needed
p.App.UnwindToPage(&Settings{})
}
}
return nil
}
// toggleProclamation immediately updates the database for the proclamation at the given display index
func (p *ProclamationOptions) toggleProclamation(displayIndex int) error {
if displayIndex < 0 || displayIndex >= len(p.proclamationIDs) {
return fmt.Errorf("invalid display index %d", displayIndex)
}
dbID := p.proclamationIDs[displayIndex]
isSelected := p.activeMap[displayIndex]
// Get the proclamation from database
var proclamation db.Proclamations
if err := db.DB.First(&proclamation, dbID).Error; err != nil {
return fmt.Errorf("retrieving proclamation %d: %w", dbID, err)
}
// Update the Selected field
proclamation.Selected = isSelected
if err := db.DB.Save(&proclamation).Error; err != nil {
return fmt.Errorf("updating proclamation %d: %w", dbID, err)
}
return nil
}
func (p *ProclamationOptions) 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)
// No proclamations to display
if len(p.proclamations) == 0 {
return nil
}
// Render highlight box behind selected item
p.RenderSolidColorQuad(
p.solidShader,
p.proclamations[p.highlightIndex].Position(),
p.proclamations[p.highlightIndex].RawDimensions(),
colors.White,
)
// Render proclamation items with appropriate colors
for i, item := range p.proclamations {
if p.activeMap[i] {
// Selected items are green
item.Color = colors.Green
} else if i == p.highlightIndex {
// Highlighted item is black (shows on white background)
item.Color = colors.Black
} else {
// Normal items are white
item.Color = colors.White
}
p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color)
}
return nil
}
func (p *ProclamationOptions) Destroy() error {
gl.DeleteTextures(1, &p.title.ID)
gl.DeleteTextures(1, &p.prompt.ID)
for _, item := range p.proclamations {
gl.DeleteTextures(1, &item.ID)
}
return nil
}
+697
View File
@@ -0,0 +1,697 @@
// ATTENTION: TED
//
// # Integrated Multi-Domain Rhema Wave Generation System
//
// This module implements the complete Rhema Wave generation system by synchronizing multiple
// subsystems that operate simultaneously across spatial, temporal, and chromatic
// domains to produce a unified frequency-structured visual output for subliminal proclamation display.
//
// The technology addresses the fundamental challenge of presenting textual proclamation content
// in a format optimized for subconscious processing rather than conscious reading. Conventional
// subliminal text systems use isolated techniques (simple masking, single-frequency flashing, or
// uniform scrolling) that operate in only one domain. Our system integrates four mathematically-
// synchronized subsystems, each generating Fibonacci-based patterns in different domains, creating
// a compound frequency signature designed to enhance subconscious absorption while maintaining
// conscious illegibility.
//
// The system architecture integrates:
//
// 1. Spatial Character Transformation - Each rendered character undergoes Fibonacci-based
// block scrambling (described separately), creating local spatial frequency patterns at
// the character level.
//
// 2. Spatial Line Distribution - Text lines contain varying word counts following an oscillating
// Fibonacci sequence (1, 1, 2, 3, 5, 8, 13... then reversing), creating a macro-level
// two-dimensional wave pattern in the display geometry and ensuring balanced temporal
// exposure of proclamation content segments.
//
// 3. Temporal Color Modulation - Sequential words receive colors from an intent-based palette,
// with color duration determined by an oscillating Fibonacci sequence, creating rhythmic
// chromatic variation synchronized with natural mathematical progression.
//
// 4. Temporal Velocity Modulation - Display scroll velocity oscillates between negative and
// positive extremes with acceleration/deceleration rates and hold periods, creating a
// "breathing" temporal rhythm for the entire visual field.
//
// The key innovation lies in the temporal and mathematical synchronization of these subsystems.
// All four operate on Fibonacci-derived parameters, creating harmonic relationships between the
// spatial frequencies (character scrambling, line wave patterns), chromatic frequencies (color
// duration oscillation), and temporal frequencies (scroll velocity modulation). This multi-domain
// integration produces what we term the "Rhema Wave" - a compound frequency signature where
// spatial, temporal, and chromatic patterns reinforce each other through mathematical harmony.
//
// Key differentiators from existing solutions:
// - Integrates four distinct Fibonacci-based frequency generators in a single system
// - Synchronizes pattern generation across spatial, temporal, and chromatic domains
// - Creates compound frequency signatures rather than isolated single-domain effects
// - Maintains character-specific and content-specific mathematical structure throughout
// - Uses bidirectional oscillation across multiple subsystems for rhythmic coherence
// - Combines macro-level (line patterns, velocity) and micro-level (character, color) modulation
//
// The system loads proclamation text content, processes it through all four subsystems simultaneously,
// and generates a continuously scrolling display where every element - from individual character
// pixels to overall scroll rhythm - contributes to the unified Rhema Wave output. The mathematical
// relationships between subsystems create interference patterns and harmonic resonances designed
// to enhance subconscious processing of the proclamation content while maintaining conscious
// illegibility through the character scrambling subsystem.
package page
import (
"fmt"
"math"
"path/filepath"
"strconv"
"strings"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/mixer"
colorPattern "rhemawavetech.com/rhemawave/internal/pattern/color"
linePattern "rhemawavetech.com/rhemawave/internal/pattern/line"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type Line struct {
words []StringItem
width int32
}
// LineDeque is a double-ended queue for Line values backed by a circular buffer.
// It avoids O(n) prepend and prevents capacity leaks from head-of-slice removal.
type LineDeque struct {
buf []Line
head, tail int
count int
}
func NewLineDeque(cap int) *LineDeque {
if cap < 4 {
cap = 4
}
return &LineDeque{buf: make([]Line, cap)}
}
func (d *LineDeque) Len() int { return d.count }
func (d *LineDeque) grow() {
newCap := len(d.buf) * 2
newBuf := make([]Line, newCap)
for i := 0; i < d.count; i++ {
newBuf[i] = d.buf[(d.head+i)%len(d.buf)]
}
d.buf = newBuf
d.head = 0
d.tail = d.count
}
func (d *LineDeque) PushBack(l Line) {
if d.count == len(d.buf) {
d.grow()
}
d.buf[d.tail] = l
d.tail = (d.tail + 1) % len(d.buf)
d.count++
}
func (d *LineDeque) PushFront(l Line) {
if d.count == len(d.buf) {
d.grow()
}
d.head = (d.head - 1 + len(d.buf)) % len(d.buf)
d.buf[d.head] = l
d.count++
}
func (d *LineDeque) PopFront() Line {
l := d.buf[d.head]
d.buf[d.head] = Line{} // clear for GC
d.head = (d.head + 1) % len(d.buf)
d.count--
return l
}
func (d *LineDeque) PopBack() Line {
d.tail = (d.tail - 1 + len(d.buf)) % len(d.buf)
l := d.buf[d.tail]
d.buf[d.tail] = Line{} // clear for GC
d.count--
return l
}
func (d *LineDeque) Front() Line { return d.buf[d.head] }
func (d *LineDeque) Back() Line {
return d.buf[(d.tail-1+len(d.buf))%len(d.buf)]
}
func (d *LineDeque) Get(i int) *Line {
return &d.buf[(d.head+i)%len(d.buf)]
}
func (d *LineDeque) Set(i int, l Line) {
d.buf[(d.head+i)%len(d.buf)] = l
}
func (d *LineDeque) Clear() {
for i := 0; i < d.count; i++ {
d.buf[(d.head+i)%len(d.buf)] = Line{}
}
d.head, d.tail, d.count = 0, 0, 0
}
type ScrollerPage struct {
Base
words []string
wordIndex int
lines *LineDeque
lineHeight int32
fontScale float32
shader *shaderManager.Shader
activePattern linePattern.LinePattern
font *fontManager.FontEntry
spaceWidth int32
lineCtx *linePattern.LineRenderContext
colorPattern *colorPattern.ColorPattern
scrollOffset float32
scrollY float32
increment float32
velocity float32
// Time-based velocity control
velocityFloor float32
velocityCeiling float32
velocityStep float32
velocityDelay float32
velocityDirection int
velocityTimer float32
velocityState int
autoVelocity bool
hold float32
}
func (p *ScrollerPage) Init(app ApplicationInterface) error {
p.increment = 100.0
p.velocity = 800.0
p.SetAutoVelocity(true, -4000.0, 4000.0, 100.0, 2.5, 6.0)
sm := shaderManager.Get()
fm := fontManager.Get()
// Get font
font, ok := fm.GetFont("Scrambled")
if !ok {
return fmt.Errorf("unable to fetch font 'Scrambled'")
}
p.font = font
p.Base.Init(app)
// Get text shader
textShader, ok := sm.Get("text")
if !ok {
return fmt.Errorf("text shader not found")
}
p.shader = textShader
// Load proclamation words from database
if err := p.loadProclamations(); err != nil {
// Don't stop execution, just show fallback words
p.words = strings.Fields("No Proclamations Selected!")
}
p.wordIndex = 0
// Get line pattern from database
linePatternName, err := db.GetConfigValue("line_pattern")
if err != nil {
return fmt.Errorf("retrieving line_pattern from db: %w", err)
}
p.activePattern, err = linePattern.Get(linePatternName)
if err != nil {
return fmt.Errorf("failed to load line pattern '%s': %w", linePatternName, err)
}
// Initialize color pattern
var baseColors = []sdl.Color{
colors.Tan,
colors.PeachPuff,
colors.LightCyan,
colors.LightSeaGreen,
colors.MediumTurquoise,
colors.DarkCyan,
colors.MediumPurple,
}
p.colorPattern = colorPattern.NewColorPattern(baseColors)
// Get font sizes from database (optimize: load once, cache as int32)
standardFontSizeStr, err := db.GetConfigValue("standard_font_size")
if err != nil {
return fmt.Errorf("retrieving standard_font_size from db: %w", err)
}
standardFontSize, err := strconv.Atoi(standardFontSizeStr)
if err != nil {
return fmt.Errorf("parsing standard_font_size: %w", err)
}
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
// Font setup (optimized: calculate once, reuse)
p.fontScale = float32(standardFontSize) / float32(baseFontSize)
nLines := int(p.ScreenHeight / int32(standardFontSize))
p.lines = NewLineDeque(nLines * 2)
// Calculate space width once
spaceGlyph, ok := fm.GetGlyph(p.font.Name, ' ')
if !ok {
return fmt.Errorf("failed to get space glyph")
}
p.spaceWidth = int32(float32(spaceGlyph.Advance) * p.fontScale)
// Get line height from a sample string
tmpStrID, _, H, err := fm.NewCreateStringTexture(p.font.Name, "Mg", p.shader)
if err != nil {
return fmt.Errorf("failed to create sample texture: %w", err)
}
p.lineHeight = int32(float32(H) * p.fontScale)
gl.DeleteTextures(1, &tmpStrID)
p.lineCtx = &linePattern.LineRenderContext{
ScreenWidth: p.ScreenWidth,
Font: p.font,
FontScale: p.fontScale,
SpaceWidth: p.spaceWidth,
}
// Create initial lines
for i := 0; i < nLines; i++ {
if err := p.createLine(i, true); err != nil {
return fmt.Errorf("failed to create initial line %d: %w", i, err)
}
}
// Audio setup
audioDeviceName, err := db.GetConfigValue("audio_device_name")
if err != nil {
return fmt.Errorf("retrieving audio_device_name from db: %w", err)
}
// Switch audio devices if needed
if mixer.CurrentDevice() != audioDeviceName {
if err := mixer.SwitchDevice(audioDeviceName); err != nil {
} else {
}
}
// Start audio playback
selectedAudio, err := db.GetConfigValue("selected_audio")
if err != nil {
return fmt.Errorf("retrieving selected_audio from db: %w", err)
}
assetsDir, err := db.GetConfigValue("assets_dir")
if err != nil {
return fmt.Errorf("retrieving assets_dir from db: %w", err)
}
if selectedAudio == "" {
} else {
if err := mixer.Play(filepath.Join(assetsDir, selectedAudio), true); err != nil {
}
}
currentDevice := mixer.CurrentDevice()
if currentDevice == "" {
currentDevice = "default"
}
return nil
}
// loadProclamations loads selected proclamations from the database
func (p *ScrollerPage) loadProclamations() error {
p.words = []string{}
// Get all selected proclamations from database
proclamations, err := db.GetProclamations()
if err != nil {
return fmt.Errorf("retrieving proclamations from db: %w", err)
}
// Build word list from selected, available proclamations
for _, proclamation := range proclamations {
if proclamation.Selected && proclamation.Available {
// Split content into words and add to list
words := strings.Fields(proclamation.Content)
p.words = append(p.words, words...)
}
}
if len(p.words) == 0 {
p.words = strings.Fields("No Proclamations Selected!")
return fmt.Errorf("no proclamations selected or available")
}
return nil
}
// createLine is a helper to generate a new line at a specific index
// isInitial determines if this is during initialization (positions from top)
// or dynamic creation (positions relative to existing lines)
func (p *ScrollerPage) createLine(index int, isInitial bool) error {
words := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx)
line := Line{words: []StringItem{}}
for _, word := range words {
if word == "" {
continue
}
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
if err != nil {
return fmt.Errorf("unable to create string item for word '%s': %w", word, err)
}
item.W = int32(float32(item.W) * p.fontScale)
item.H = int32(float32(item.H) * p.fontScale)
if len(line.words) > 0 {
line.width += p.spaceWidth
}
line.words = append(line.words, item)
line.width += item.W
}
// Calculate horizontal position (centered)
startX := p.ScreenCenterX - (line.width / 2)
// Calculate vertical position
var baseY int32
if isInitial {
baseY = int32(p.scrollY) - int32(p.lineHeight)*int32(index)
} else {
baseY = 0 // Will be set by caller based on context
}
// Position all words in the line
for j := range line.words {
if j == 0 {
line.words[j].X = startX
} else {
line.words[j].X = line.words[j-1].X + line.words[j-1].W + p.spaceWidth
}
line.words[j].Y = baseY
}
if isInitial {
p.lines.PushBack(line)
}
return nil
}
func (p *ScrollerPage) HandleEvent(event *sdl.Event) error {
switch e := (*event).(type) {
case *sdl.KeyboardEvent:
if e.Type != sdl.KEYDOWN {
return nil
}
switch e.Keysym.Sym {
case sdl.K_UP:
if !p.autoVelocity {
p.velocity += p.increment
}
case sdl.K_DOWN:
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
}
return nil
}
func (p *ScrollerPage) Update(dt float32) error {
p.updateAutoVelocity(dt)
p.scrollOffset += dt * p.velocity
move := int32(p.scrollOffset)
if move == 0 {
return nil
}
p.scrollOffset -= float32(move)
// Move all lines vertically
for i := 0; i < p.lines.Len(); i++ {
line := p.lines.Get(i)
for j := range line.words {
line.words[j].Y += move
}
}
// Handle line replacement based on scroll direction
if p.velocity > 0 {
// Moving up: replace lines that go off top
for p.lines.Len() > 0 && len(p.lines.Front().words) > 0 && p.lines.Front().words[0].Y > p.ScreenHeight {
old := p.lines.PopFront()
for _, word := range old.words {
gl.DeleteTextures(1, &word.ID)
}
if err := p.addLineAtBottom(); err != nil {
return err
}
}
} else if p.velocity < 0 {
// Moving down: replace lines that go off bottom
for p.lines.Len() > 0 {
back := p.lines.Back()
if len(back.words) == 0 || back.words[0].Y+back.words[0].H >= 0 {
break
}
old := p.lines.PopBack()
for _, word := range old.words {
gl.DeleteTextures(1, &word.ID)
}
if err := p.addLineAtTop(); err != nil {
return err
}
}
}
return nil
}
// addLineAtBottom creates and adds a new line at the bottom of the display
func (p *ScrollerPage) addLineAtBottom() error {
newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx)
line := Line{words: []StringItem{}}
for _, word := range newWords {
if word == "" {
continue
}
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
if err != nil {
return fmt.Errorf("unable to create string item for word '%s': %w", word, err)
}
item.W = int32(float32(item.W) * p.fontScale)
item.H = int32(float32(item.H) * p.fontScale)
if len(line.words) > 0 {
line.width += p.spaceWidth
}
line.words = append(line.words, item)
line.width += item.W
}
startX := p.ScreenCenterX - (line.width / 2)
// Position relative to last existing line
var baseY int32
if p.lines.Len() > 0 && len(p.lines.Back().words) > 0 {
baseY = p.lines.Back().words[0].Y - int32(p.lineHeight)
} else {
baseY = -int32(p.lineHeight)
}
for j := range line.words {
if j == 0 {
line.words[j].X = startX
} else {
line.words[j].X = line.words[j-1].X + line.words[j-1].W + p.spaceWidth
}
line.words[j].Y = baseY
}
p.lines.PushBack(line)
return nil
}
// addLineAtTop creates and adds a new line at the top of the display
func (p *ScrollerPage) addLineAtTop() error {
newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx)
line := Line{words: []StringItem{}}
for _, word := range newWords {
if word == "" {
continue
}
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
if err != nil {
return fmt.Errorf("unable to create string item for word '%s': %w", word, err)
}
item.W = int32(float32(item.W) * p.fontScale)
item.H = int32(float32(item.H) * p.fontScale)
if len(line.words) > 0 {
line.width += p.spaceWidth
}
line.words = append(line.words, item)
line.width += item.W
}
startX := p.ScreenCenterX - (line.width / 2)
// Position relative to first existing line
var baseY int32
if p.lines.Len() > 0 && len(p.lines.Front().words) > 0 {
baseY = p.lines.Front().words[0].Y + int32(p.lineHeight)
} else {
baseY = p.ScreenHeight
}
for j := range line.words {
if j == 0 {
line.words[j].X = startX
} else {
line.words[j].X = line.words[j-1].X + line.words[j-1].W + p.spaceWidth
}
line.words[j].Y = baseY
}
p.lines.PushFront(line)
return nil
}
func (p *ScrollerPage) Render() error {
p.BeginTextureRendering(p.shader)
for i := 0; i < p.lines.Len(); i++ {
line := p.lines.Get(i)
for _, word := range line.words {
if word.ID == 0 {
continue
}
p.RenderTextureBatched(p.shader, word.ID, word.Position(), word.RawDimensions(), word.Color)
}
}
p.EndTextureRendering()
return nil
}
func (p *ScrollerPage) Destroy() error {
if p == nil {
return nil
}
mixer.Stop(1000)
// GL cleanup — delete all text textures
for i := 0; i < p.lines.Len(); i++ {
line := p.lines.Get(i)
for j := range line.words {
if id := line.words[j].ID; id != 0 {
gl.DeleteTextures(1, &line.words[j].ID)
line.words[j].ID = 0
}
}
}
gl.Flush()
// Drop references for GC
p.lines = nil
p.words = nil
p.lineCtx = nil
p.font = nil
p.shader = nil
p.colorPattern = nil
return nil
}
// SetAutoVelocity configures the automatic velocity system for the ScrollerPage.
func (p *ScrollerPage) SetAutoVelocity(enabled bool, floor, ceiling, step, delay, hold float32) {
p.autoVelocity = enabled
p.velocityFloor = -float32(math.Abs(float64(floor)))
p.velocityCeiling = float32(math.Abs(float64(ceiling)))
p.velocityStep = float32(math.Abs(float64(step)))
p.velocityDelay = delay
p.hold = hold
// Initialize state
p.velocityDirection = 1
p.velocityState = 0
p.velocityTimer = 0.0
p.velocity = p.velocityFloor
}
// updateAutoVelocity handles automatic velocity changes
func (p *ScrollerPage) updateAutoVelocity(dt float32) {
if !p.autoVelocity {
return
}
switch p.velocityState {
case 0: // Changing velocity
if p.velocityDirection == 1 {
p.velocity += p.velocityStep * dt
if p.velocity >= p.velocityCeiling {
p.velocity = p.velocityCeiling
p.velocityState = 1
p.velocityTimer = 0.0
}
} else {
p.velocity -= p.velocityStep * dt
if p.velocity <= p.velocityFloor {
p.velocity = p.velocityFloor
p.velocityState = 2
p.velocityTimer = 0.0
}
}
case 1: // Waiting at ceiling
p.velocityTimer += dt
if p.velocityTimer >= p.velocityDelay {
p.velocityDirection = -1
p.velocityState = 0
}
case 2: // Waiting at floor
p.velocityTimer += dt
if p.velocityTimer >= p.velocityDelay {
p.velocityDirection = 1
p.velocityState = 0
}
}
}
+253
View File
@@ -0,0 +1,253 @@
package page
import (
"fmt"
"strconv"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type Settings struct {
Base
font *fontManager.FontEntry
textShader *shaderManager.Shader
solidShader *shaderManager.Shader
title StringItem
prompt StringItem
options []Option
verticalMargin int32
bgHighlight PageItem
highlightIndex int
}
type Option struct {
page Page
label StringItem
}
func (p *Settings) Init(app ApplicationInterface) error {
var err error
p.verticalMargin = int32(20)
// Get font
fm := fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return fmt.Errorf("failed to get font: %w", sdl.GetError())
}
p.font = font
// Get shaders
sm := shaderManager.Get()
p.textShader, ok = sm.Get("text")
if !ok {
return fmt.Errorf("failed to get text shader: %w", sdl.GetError())
}
p.solidShader, ok = sm.Get("solid_color")
if !ok {
return fmt.Errorf("failed to get solid_color shader: %w", sdl.GetError())
}
// Initialize base
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
// Create title
p.title, err = NewStringItem("Settings", font, colors.White)
if err != nil {
return fmt.Errorf("creating title: %w", err)
}
// Create prompt
p.prompt, err = NewStringItem("Use Arrow Keys to Navigate, Enter to Select, or F1 to return to the Main Menu", font, colors.White)
if err != nil {
return fmt.Errorf("creating prompt: %w", err)
}
// Create options
proclamationsLabel, err := NewStringItem("Proclamations", font, colors.White)
if err != nil {
return fmt.Errorf("creating proclamations label: %w", err)
}
p.options = append(p.options, Option{
page: &ProclamationOptions{},
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{
page: &WiFiSetupPage{},
label: wifiLabel,
})
audioLabel, err := NewStringItem("Audio Device", font, colors.White)
if err != nil {
return fmt.Errorf("creating audio label: %w", err)
}
p.options = append(p.options, Option{
page: &AudioDevices{},
label: audioLabel,
})
emailLabel, err := NewStringItem("Email Address", font, colors.White)
if err != nil {
return fmt.Errorf("creating email label: %w", err)
}
p.options = append(p.options, Option{
page: &EmailAddressPage{},
label: emailLabel,
})
licenseKeyLabel, err := NewStringItem("License Key", font, colors.White)
if err != nil {
return fmt.Errorf("creating license key label: %w", err)
}
p.options = append(p.options, Option{
page: &LicenseKeyPage{},
label: licenseKeyLabel,
})
logsLabel, err := NewStringItem("Logs", font, colors.White)
if err != nil {
return fmt.Errorf("creating logs label: %w", err)
}
p.options = append(p.options, Option{
page: &LogViewerPage{},
label: logsLabel,
})
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{
page: &UpdateCheckPage{},
label: updateLabel,
})
// Get base font size from database
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
// Calculate scales
titleScale := float32(64) / float32(baseFontSize)
scale := float32(32) / float32(baseFontSize)
// Apply scales
p.title.Scale(titleScale)
p.prompt.Scale(scale)
for i := range p.options {
p.options[i].label.Scale(scale)
}
// Create background highlight using first option's dimensions
p.bgHighlight = PageItem{
ID: 0,
W: p.options[0].label.W,
H: p.options[0].label.H,
Color: colors.White,
}
p.highlightIndex = 0
// Position title at bottom
p.title.X = p.Base.ScreenCenterX - p.title.W/2
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.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H
// Position options stacked upward from prompt
for i, option := range p.options {
p.options[i].label.X = p.ScreenCenterX - option.label.W/2
if i == 0 {
p.options[i].label.Y = int32(float32(p.prompt.Y)-(float32(p.verticalMargin)*4)) - option.label.H
} else {
p.options[i].label.Y = p.options[i-1].label.Y - option.label.H - 10
}
}
return nil
}
func (p *Settings) Update(dt float32) error {
return nil
}
func (p *Settings) HandleEvent(event *sdl.Event) error {
switch e := (*event).(type) {
case *sdl.KeyboardEvent:
if e.Type != sdl.KEYDOWN {
return nil
}
switch e.Keysym.Sym {
case sdl.K_UP:
if p.highlightIndex > 0 {
p.highlightIndex--
}
case sdl.K_DOWN:
if p.highlightIndex < len(p.options)-1 {
p.highlightIndex++
}
case sdl.K_RETURN:
if p.highlightIndex >= 0 && p.highlightIndex < len(p.options) {
selectedOption := p.options[p.highlightIndex]
p.App.SwitchPage(selectedOption.page)
}
case sdl.K_BACKSPACE:
p.App.SwitchPage(&Welcome{})
}
}
return nil
}
func (p *Settings) Render() error {
// 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 behind selected option
p.RenderSolidColorQuad(
p.solidShader,
p.options[p.highlightIndex].label.Position(),
p.options[p.highlightIndex].label.RawDimensions(),
p.bgHighlight.Color,
)
// Render all options with appropriate colors
for i, option := range p.options {
if p.highlightIndex == i {
option.label.Color = colors.Black
} else {
option.label.Color = colors.White
}
p.RenderTexture(p.textShader, option.label.ID, option.label.Position(), option.label.RawDimensions(), option.label.Color)
}
return nil
}
func (p *Settings) Destroy() error {
gl.DeleteTextures(1, &p.title.ID)
gl.DeleteTextures(1, &p.prompt.ID)
for i := range p.options {
gl.DeleteTextures(1, &p.options[i].label.ID)
}
return nil
}
+388
View File
@@ -0,0 +1,388 @@
package page
import (
"fmt"
"strconv"
"strings"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/logger"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type TOSPage struct {
Base
Title string
Content string
ConfigKey string
titleTextureID uint32
titleTextureWidth int32
titleTextureHeight int32
lines []string
lineTextureIDs []uint32
lineTextureWidths []int32
lineTextureHeights []int32
promptTextureID uint32
promptTextureWidth int32
promptTextureHeight int32
scrollHintTextureID uint32
scrollHintTextureWidth int32
scrollHintTextureHeight int32
scrollOffset int32
maxScrollOffset int32
lineHeight int32
visibleHeight int32
}
func (p *TOSPage) Init(app ApplicationInterface) error {
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
p.lines = parseHTMLToLines(p.Content, 60)
applicationFont, err := db.GetConfigValue("application_font")
if err != nil {
return fmt.Errorf("retrieving application_font from db: %w", err)
}
fm := fontManager.Get()
textShader, ok := shaderManager.Get().Get("text")
if !ok {
return fmt.Errorf("unable to fetch text shader")
}
titleTextureID, titleWidth, titleHeight, err := fm.CreateStringTexture(applicationFont, p.Title, textShader)
if err != nil {
return fmt.Errorf("failed to create title texture: %w", err)
}
p.titleTextureID = titleTextureID
p.titleTextureWidth = titleWidth
p.titleTextureHeight = titleHeight
p.lineTextureIDs = make([]uint32, len(p.lines))
p.lineTextureWidths = make([]int32, len(p.lines))
p.lineTextureHeights = make([]int32, len(p.lines))
for i, line := range p.lines {
if strings.TrimSpace(line) == "" {
continue
}
texID, w, h, err := fm.CreateStringTexture(applicationFont, line, textShader)
if err != nil {
return fmt.Errorf("failed to create line texture: %w", err)
}
p.lineTextureIDs[i] = texID
p.lineTextureWidths[i] = w
p.lineTextureHeights[i] = h
}
promptText := "Press Y to Accept | Press N to Decline"
promptTextureID, promptWidth, promptHeight, err := fm.CreateStringTexture(applicationFont, promptText, textShader)
if err != nil {
return fmt.Errorf("failed to create prompt texture: %w", err)
}
p.promptTextureID = promptTextureID
p.promptTextureWidth = promptWidth
p.promptTextureHeight = promptHeight
scrollHintText := "Scroll down: Arrow Keys / PgDn"
scrollHintID, scrollHintW, scrollHintH, err := fm.CreateStringTexture(applicationFont, scrollHintText, textShader)
if err != nil {
return fmt.Errorf("failed to create scroll hint texture: %w", err)
}
p.scrollHintTextureID = scrollHintID
p.scrollHintTextureWidth = scrollHintW
p.scrollHintTextureHeight = scrollHintH
return nil
}
// parseHTMLToLines converts HTML content to plain-text lines for rendering.
// Supported tags: <p>, <br>, <li>, <h1>-<h6>. All other tags are stripped.
// HTML entities &amp; &lt; &gt; &quot; &#39; are decoded.
func parseHTMLToLines(html string, maxCharsPerLine int) []string {
var lines []string
// Replace <br>, <br/>, <br /> with newline markers
br := strings.NewReplacer(
"<br>", "\n", "<br/>", "\n", "<br />", "\n",
"<BR>", "\n", "<BR/>", "\n", "<BR />", "\n",
)
html = br.Replace(html)
// Process block-level tags
// Replace </p>, </h1>-</h6> with paragraph break marker
for _, tag := range []string{"</p>", "</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>"} {
html = strings.ReplaceAll(html, tag, "\n\n")
html = strings.ReplaceAll(html, strings.ToUpper(tag), "\n\n")
}
// Replace <li> with list prefix marker
html = strings.ReplaceAll(html, "<li>", "\n- ")
html = strings.ReplaceAll(html, "<LI>", "\n- ")
// Strip all remaining HTML tags
var stripped strings.Builder
inTag := false
for _, r := range html {
if r == '<' {
inTag = true
continue
}
if r == '>' {
inTag = false
continue
}
if !inTag {
stripped.WriteRune(r)
}
}
text := stripped.String()
// Decode HTML entities
text = strings.ReplaceAll(text, "&amp;", "&")
text = strings.ReplaceAll(text, "&lt;", "<")
text = strings.ReplaceAll(text, "&gt;", ">")
text = strings.ReplaceAll(text, "&quot;", "\"")
text = strings.ReplaceAll(text, "&#39;", "'")
// Split into segments on double-newline (paragraph breaks) and single newline (line breaks)
paragraphs := strings.Split(text, "\n\n")
for i, para := range paragraphs {
para = strings.TrimSpace(para)
if para == "" {
continue
}
// Handle single newlines within a paragraph (from <br> tags)
sublines := strings.Split(para, "\n")
for _, sub := range sublines {
sub = strings.TrimSpace(sub)
if sub == "" {
continue
}
wrapped := wrapText(sub, maxCharsPerLine)
lines = append(lines, strings.Split(wrapped, "\n")...)
}
// Add paragraph break (empty line) between paragraphs
if i < len(paragraphs)-1 {
lines = append(lines, "")
}
}
return lines
}
func wrapText(text string, maxCharsPerLine int) string {
var result strings.Builder
words := strings.Fields(text)
lineLen := 0
for _, word := range words {
if lineLen+len(word)+1 > maxCharsPerLine && lineLen > 0 {
result.WriteString("\n")
lineLen = 0
}
if lineLen > 0 {
result.WriteString(" ")
lineLen++
}
result.WriteString(word)
lineLen += len(word)
}
return result.String()
}
func (p *TOSPage) HandleEvent(event *sdl.Event) error {
actualEvent := *event
switch e := actualEvent.(type) {
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
switch e.Keysym.Sym {
case sdl.K_n:
logger.Info("User declined TOS (%s); exiting", p.ConfigKey)
p.App.Quit()
case sdl.K_y:
if p.scrollOffset < p.maxScrollOffset {
return nil
}
logger.Info("User accepted TOS (%s)", p.ConfigKey)
if err := db.SetConfigValue(p.ConfigKey, "true"); err != nil {
return fmt.Errorf("failed to save TOS acceptance (%s): %w", p.ConfigKey, err)
}
p.App.SchedulePop()
case sdl.K_UP:
p.scrollOffset -= p.lineHeight
if p.scrollOffset < 0 {
p.scrollOffset = 0
}
case sdl.K_DOWN:
p.scrollOffset += p.lineHeight
if p.scrollOffset > p.maxScrollOffset {
p.scrollOffset = p.maxScrollOffset
}
case sdl.K_PAGEUP:
p.scrollOffset -= p.visibleHeight
if p.scrollOffset < 0 {
p.scrollOffset = 0
}
case sdl.K_PAGEDOWN:
p.scrollOffset += p.visibleHeight
if p.scrollOffset > p.maxScrollOffset {
p.scrollOffset = p.maxScrollOffset
}
case sdl.K_HOME:
p.scrollOffset = 0
case sdl.K_END:
p.scrollOffset = p.maxScrollOffset
}
}
case *sdl.MouseWheelEvent:
p.scrollOffset -= e.Y * p.lineHeight * 3
if p.scrollOffset < 0 {
p.scrollOffset = 0
}
if p.scrollOffset > p.maxScrollOffset {
p.scrollOffset = p.maxScrollOffset
}
}
return nil
}
func (p *TOSPage) Update(dt float32) error {
return nil
}
func (p *TOSPage) Render() error {
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
sm := shaderManager.Get()
textShader, _ := sm.Get("text")
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
grey := sdl.Color{R: 180, G: 180, B: 180, A: 255}
titleFontSize := int32(48)
titleScale := float32(titleFontSize) / float32(baseFontSize)
contentFontSize := int32(32)
contentScale := float32(contentFontSize) / float32(baseFontSize)
promptFontSize := int32(20)
promptScale := float32(promptFontSize) / float32(baseFontSize)
margin := int32(40)
lineSpacing := int32(4)
paragraphSpacing := int32(16)
p.lineHeight = contentFontSize + lineSpacing
// Center a content column at ~55% of screen width, left-align text within it
contentColumnWidth := int32(float32(p.Base.ScreenWidth) * 0.55)
contentLeftX := p.Base.ScreenCenterX - contentColumnWidth/2
// Title
titleH := float32(p.titleTextureHeight) * titleScale
titleW := float32(p.titleTextureWidth) * titleScale
titleX := p.Base.ScreenCenterX - int32(titleW/2)
titleY := p.Base.ScreenHeight - margin - int32(titleH)
p.Base.RenderTexture(textShader, p.titleTextureID, mgl32.Vec2{float32(titleX), float32(titleY)}, mgl32.Vec2{titleW, titleH}, white)
// Prompt dimensions (calculate even if not shown, for content bounds)
promptH := float32(p.promptTextureHeight) * promptScale
promptW := float32(p.promptTextureWidth) * promptScale
promptX := p.Base.ScreenCenterX - int32(promptW/2)
promptY := margin
// Content area bounds
contentTop := titleY - margin
contentBottom := promptY + int32(promptH) + margin
p.visibleHeight = contentTop - contentBottom
// Calculate total content height (accounting for paragraph breaks)
totalContentHeight := int32(0)
for _, line := range p.lines {
if strings.TrimSpace(line) == "" {
totalContentHeight += paragraphSpacing
} else {
totalContentHeight += p.lineHeight
}
}
p.maxScrollOffset = totalContentHeight - p.visibleHeight
if p.maxScrollOffset < 0 {
p.maxScrollOffset = 0
}
// Render visible lines
currentY := contentTop - p.lineHeight + p.scrollOffset
for i, line := range p.lines {
if strings.TrimSpace(line) == "" {
currentY -= paragraphSpacing
continue
}
if p.lineTextureIDs[i] == 0 {
currentY -= p.lineHeight
continue
}
// Skip lines outside visible area
if currentY > contentTop || currentY+p.lineHeight < contentBottom {
currentY -= p.lineHeight
continue
}
lineW := float32(p.lineTextureWidths[i]) * contentScale
lineH := float32(p.lineTextureHeights[i]) * contentScale
p.Base.RenderTexture(textShader, p.lineTextureIDs[i], mgl32.Vec2{float32(contentLeftX), float32(currentY)}, mgl32.Vec2{lineW, lineH}, white)
currentY -= p.lineHeight
}
// Show accept/decline prompt when scrolled to bottom, scroll hint otherwise
if p.scrollOffset >= p.maxScrollOffset {
p.Base.RenderTexture(textShader, p.promptTextureID, mgl32.Vec2{float32(promptX), float32(promptY)}, mgl32.Vec2{promptW, promptH}, grey)
} else {
hintScale := promptScale
hintW := float32(p.scrollHintTextureWidth) * hintScale
hintH := float32(p.scrollHintTextureHeight) * hintScale
hintX := p.Base.ScreenCenterX - int32(hintW/2)
hintY := promptY
p.Base.RenderTexture(textShader, p.scrollHintTextureID, mgl32.Vec2{float32(hintX), float32(hintY)}, mgl32.Vec2{hintW, hintH}, grey)
}
return nil
}
func (p *TOSPage) Destroy() error {
gl.DeleteTextures(1, &p.titleTextureID)
gl.DeleteTextures(1, &p.promptTextureID)
gl.DeleteTextures(1, &p.scrollHintTextureID)
for _, texID := range p.lineTextureIDs {
if texID != 0 {
gl.DeleteTextures(1, &texID)
}
}
return p.Base.Destroy()
}
+196
View File
@@ -0,0 +1,196 @@
package page
import (
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
type updateCheckState int
const (
updateCheckStateChecking updateCheckState = iota
updateCheckStateDone
)
type UpdateCheckPage struct {
Base
textShader *shaderManager.Shader
state updateCheckState
// "Checking for Updates." / ".." / "..."
checkingTextures [3]StringItem
// "No updates available" and "Press F3 to return"
doneMessage StringItem
doneHint StringItem
// Error message if service failed to start
errorMessage StringItem
hasError bool
pollTimer float32
dotIndex int
dotTimer float32
}
func (p *UpdateCheckPage) Init(app ApplicationInterface) error {
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
fm := fontManager.Get()
sm := shaderManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return fmt.Errorf("failed to get font")
}
p.textShader, ok = sm.Get("text")
if !ok {
return fmt.Errorf("failed to get text shader")
}
// Get scale
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
scale := float32(48) / float32(baseFontSize)
// Create checking textures with animated dots
dots := []string{"Checking for Updates.", "Checking for Updates..", "Checking for Updates..."}
for i, text := range dots {
item, err := NewStringItem(text, font, colors.White)
if err != nil {
return fmt.Errorf("creating checking texture %d: %w", i, err)
}
item.Scale(scale)
p.checkingTextures[i] = item
}
// Create done textures
hintScale := float32(32) / float32(baseFontSize)
p.doneMessage, err = NewStringItem("No updates available", font, colors.White)
if err != nil {
return fmt.Errorf("creating done message: %w", err)
}
p.doneMessage.Scale(scale)
p.doneHint, err = NewStringItem("Press F3 to return", font, colors.White)
if err != nil {
return fmt.Errorf("creating done hint: %w", err)
}
p.doneHint.Scale(hintScale)
// Start the update service
p.state = updateCheckStateChecking
if err := exec.Command("systemctl", "start", "rhemawave-update.service").Run(); err != nil {
p.errorMessage, _ = NewStringItem("Update service unavailable", font, colors.White)
p.errorMessage.Scale(scale)
p.hasError = true
p.state = updateCheckStateDone
}
return nil
}
func (p *UpdateCheckPage) Update(dt float32) error {
if p.state != updateCheckStateChecking {
return nil
}
// Animate dots
p.dotTimer += dt
if p.dotTimer >= 0.5 {
p.dotTimer -= 0.5
p.dotIndex = (p.dotIndex + 1) % 3
}
// Poll service status every 2 seconds
p.pollTimer += dt
if p.pollTimer >= 2.0 {
p.pollTimer -= 2.0
output, _ := exec.Command("systemctl", "is-active", "rhemawave-update.service").Output()
status := strings.TrimSpace(string(output))
if status != "active" && status != "activating" {
p.state = updateCheckStateDone
}
}
return nil
}
func (p *UpdateCheckPage) HandleEvent(event *sdl.Event) error {
if p.state != updateCheckStateDone {
return nil
}
switch e := (*event).(type) {
case *sdl.KeyboardEvent:
if e.Type != sdl.KEYDOWN {
return nil
}
switch e.Keysym.Sym {
case sdl.K_F3, sdl.K_BACKSPACE:
p.App.UnwindToPage(&Settings{})
}
}
return nil
}
func (p *UpdateCheckPage) Render() error {
switch p.state {
case updateCheckStateChecking:
item := p.checkingTextures[p.dotIndex]
item.X = p.ScreenCenterX - item.W/2
item.Y = p.ScreenCenterY - item.H/2
p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color)
case updateCheckStateDone:
var msg *StringItem
if p.hasError {
msg = &p.errorMessage
} else {
msg = &p.doneMessage
}
msg.X = p.ScreenCenterX - msg.W/2
msg.Y = p.ScreenCenterY + msg.H/2
p.RenderTexture(p.textShader, msg.ID, msg.Position(), msg.RawDimensions(), msg.Color)
p.doneHint.X = p.ScreenCenterX - p.doneHint.W/2
p.doneHint.Y = msg.Y - p.doneHint.H - 20
p.RenderTexture(p.textShader, p.doneHint.ID, p.doneHint.Position(), p.doneHint.RawDimensions(), p.doneHint.Color)
}
return nil
}
func (p *UpdateCheckPage) Destroy() error {
for i := range p.checkingTextures {
gl.DeleteTextures(1, &p.checkingTextures[i].ID)
}
gl.DeleteTextures(1, &p.doneMessage.ID)
gl.DeleteTextures(1, &p.doneHint.ID)
if p.hasError {
gl.DeleteTextures(1, &p.errorMessage.ID)
}
return nil
}
+439
View File
@@ -0,0 +1,439 @@
package page
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
var GitVersion = "dev"
type Welcome struct {
Base
title StringItem
version StringItem
prompt StringItem
note StringItem
licenseInfoHeading StringItem
emailDisplay StringItem
licenseKeyDisplay StringItem
displayStatus StringItem
sdlDisplayStatus StringItem
hasLicenseInfo bool
lastDisplayUpdate time.Time
drmDisplayCount int
sdlDisplayCount int
baseFontSize int // cached from db at init
}
// countConnectedDisplays returns the number of connected displays via DRM
func countConnectedDisplays() int {
matches, err := filepath.Glob("/sys/class/drm/card*/status")
if err != nil {
return 0
}
connected := 0
for _, path := range matches {
content, err := os.ReadFile(path)
if err != nil {
continue
}
status := strings.TrimSpace(string(content))
if status == "connected" {
connected++
}
}
return connected
}
// countSDLDisplays returns the number of displays detected by SDL
func countSDLDisplays() int {
numDisplays, err := sdl.GetNumVideoDisplays()
if err != nil {
return 0
}
return numDisplays
}
// updateDisplayCounts updates the display count textures
func (p *Welcome) updateDisplayCounts() error {
// Get font
fm := fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return fmt.Errorf("unable to fetch font")
}
// Get base font size
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
// Update DRM count
p.drmDisplayCount = countConnectedDisplays()
// Delete old texture
if p.displayStatus.ID != 0 {
gl.DeleteTextures(1, &p.displayStatus.ID)
}
// Create new texture
displayStatusText := fmt.Sprintf("Connected Displays: %d", p.drmDisplayCount)
p.displayStatus, err = NewStringItem(displayStatusText, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create display status texture: %w", err)
}
displayStatusScale := float32(24) / float32(baseFontSize)
p.displayStatus.Scale(displayStatusScale)
// Update SDL count
p.sdlDisplayCount = countSDLDisplays()
// Delete old texture
if p.sdlDisplayStatus.ID != 0 {
gl.DeleteTextures(1, &p.sdlDisplayStatus.ID)
}
// Create new texture
sdlDisplayStatusText := fmt.Sprintf("SDL Displays Detected: %d", p.sdlDisplayCount)
p.sdlDisplayStatus, err = NewStringItem(sdlDisplayStatusText, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create SDL display status texture: %w", err)
}
p.sdlDisplayStatus.Scale(displayStatusScale)
return nil
}
// Init performs basic page initialization and creation of static assets.
func (p *Welcome) Init(app ApplicationInterface) error {
title := "Rhema Wave"
version := GitVersion
prompt := "Press F2 to Start, or F3 for Settings"
note := "You can always press F1 to return here"
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
// Get font
fm := fontManager.Get()
font, ok := fm.GetFont("Roboto-Regular")
if !ok {
return fmt.Errorf("unable to fetch font")
}
// Create title texture
var err error
p.title, err = NewStringItem(title, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create title string texture: %w", err)
}
// Create version texture
versionString := "Version: " + version
p.version, err = NewStringItem(versionString, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create version string texture: %w", err)
}
// Create prompt texture
p.prompt, err = NewStringItem(prompt, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create prompt string texture: %w", err)
}
// Create note texture
p.note, err = NewStringItem(note, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create note string texture: %w", err)
}
// Initialize display counts
p.lastDisplayUpdate = time.Now()
if err := p.updateDisplayCounts(); err != nil {
return fmt.Errorf("unable to update display counts: %w", err)
}
// Load license information from database
emailAddress, err := db.GetConfigValue("email_address")
if err != nil {
return fmt.Errorf("retrieving email_address from db: %w", err)
}
licenseKey, err := db.GetConfigValue("license_key")
if err != nil {
return fmt.Errorf("retrieving license_key from db: %w", err)
}
// Create license info textures if data exists
if emailAddress != "" || licenseKey != "" {
p.hasLicenseInfo = true
// Create heading
p.licenseInfoHeading, err = NewStringItem("License Info", font, colors.White)
if err != nil {
return fmt.Errorf("unable to create license info heading texture: %w", err)
}
// Create email display if exists
if emailAddress != "" {
emailText := fmt.Sprintf("Email: %s", emailAddress)
p.emailDisplay, err = NewStringItem(emailText, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create email display texture: %w", err)
}
}
// Create license key display if exists
if licenseKey != "" {
licenseKeyText := fmt.Sprintf("License Key: %s", licenseKey)
p.licenseKeyDisplay, err = NewStringItem(licenseKeyText, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create license key display texture: %w", err)
}
}
}
// Get base font size from database
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
p.baseFontSize = baseFontSize
// Scale the note text
fontSize := int32(32)
scale := float32(fontSize) / float32(baseFontSize)
p.note.Scale(scale * 0.75)
// Scale license info texts
if p.hasLicenseInfo {
licenseInfoScale := float32(28) / float32(baseFontSize)
p.licenseInfoHeading.Scale(licenseInfoScale)
if p.emailDisplay.ID != 0 {
p.emailDisplay.Scale(licenseInfoScale * 0.85)
}
if p.licenseKeyDisplay.ID != 0 {
p.licenseKeyDisplay.Scale(licenseInfoScale * 0.85)
}
}
return nil
}
// HandleEvent processes SDL events and handles them appropriately for the Welcome page.
func (p *Welcome) HandleEvent(event *sdl.Event) error {
return nil
}
// Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32.
func (p *Welcome) Update(dt float32) error {
// Update display counts every 2 seconds
if time.Since(p.lastDisplayUpdate) >= 2*time.Second {
if err := p.updateDisplayCounts(); err != nil {
}
p.lastDisplayUpdate = time.Now()
}
return nil
}
func (p *Welcome) Render() error {
baseFontSize := p.baseFontSize
sm := shaderManager.Get()
verticalMargin := int32(20)
// 1) Calculate scales
titleFontSize := int32(64)
bodyFontSize := int32(32)
titleScale := float32(titleFontSize) / float32(baseFontSize)
bodyScale := float32(bodyFontSize) / float32(baseFontSize)
// 2) Calculate dimensions (scaled)
titleW := float32(p.title.W) * titleScale
titleH := float32(p.title.H) * titleScale
titleDim := mgl32.Vec2{titleW, titleH}
versionW := float32(p.version.W) * bodyScale
versionH := float32(p.version.H) * bodyScale
versionDim := mgl32.Vec2{versionW, versionH}
promptW := float32(p.prompt.W) * bodyScale
promptH := float32(p.prompt.H) * bodyScale
promptDim := mgl32.Vec2{promptW, promptH}
// p.note already scaled in Init via p.note.Scale(scale*0.75)
noteDim := p.note.RawDimensions()
noteW, noteH := noteDim[0], noteDim[1]
// 3) Calculate total height of the vertical stack
totalH := int32(titleH) + verticalMargin +
int32(versionH) + verticalMargin +
int32(promptH) + verticalMargin +
int32(noteH)
// 4) Y-up layout: start from the TOP of the centered stack and step DOWN
top := p.Base.ScreenCenterY + totalH/2
titleY := top - int32(titleH) // Title sits at the top
versionY := titleY - verticalMargin - int32(versionH)
p.prompt.Y = versionY - verticalMargin - int32(promptH)
p.note.Y = p.prompt.Y - verticalMargin - int32(noteH)
// 5) Center X for each item
titleX := p.Base.ScreenCenterX - int32(titleW/2)
versionX := p.Base.ScreenCenterX - int32(versionW/2)
p.prompt.X = p.Base.ScreenCenterX - int32(promptW/2)
p.note.X = p.Base.ScreenCenterX - int32(noteW/2)
titlePos := mgl32.Vec2{float32(titleX), float32(titleY)}
versionPos := mgl32.Vec2{float32(versionX), float32(versionY)}
// 6) Position display status in bottom left corner
displayStatusDim := p.displayStatus.RawDimensions()
displayStatusH := displayStatusDim[1]
// sdlDisplayStatusDim := p.sdlDisplayStatus.RawDimensions()
// sdlDisplayStatusH := sdlDisplayStatusDim[1]
displayStatusMargin := int32(5)
p.displayStatus.X = 15
p.displayStatus.Y = 15
p.sdlDisplayStatus.X = 15
p.sdlDisplayStatus.Y = 15 + int32(displayStatusH) + displayStatusMargin
// 7) Position license info at bottom center
if p.hasLicenseInfo {
licenseInfoMargin := int32(10)
bottomMargin := int32(20)
// License info items are already scaled in Init
licenseHeadingDim := p.licenseInfoHeading.RawDimensions()
licenseHeadingW, licenseHeadingH := licenseHeadingDim[0], licenseHeadingDim[1]
var emailDisplayH, licenseKeyDisplayH float32
if p.emailDisplay.ID != 0 {
emailDisplayDim := p.emailDisplay.RawDimensions()
emailDisplayH = emailDisplayDim[1]
}
if p.licenseKeyDisplay.ID != 0 {
licenseKeyDisplayDim := p.licenseKeyDisplay.RawDimensions()
licenseKeyDisplayH = licenseKeyDisplayDim[1]
}
// Calculate total height of license info block
licenseInfoTotalH := int32(licenseHeadingH)
if p.emailDisplay.ID != 0 {
licenseInfoTotalH += int32(emailDisplayH) + licenseInfoMargin
}
if p.licenseKeyDisplay.ID != 0 {
licenseInfoTotalH += int32(licenseKeyDisplayH) + licenseInfoMargin
}
// Start from bottom and work up
currentY := bottomMargin
// License key (bottom-most if it exists)
if p.licenseKeyDisplay.ID != 0 {
licenseKeyDisplayDim := p.licenseKeyDisplay.RawDimensions()
licenseKeyDisplayW := licenseKeyDisplayDim[0]
p.licenseKeyDisplay.X = p.Base.ScreenCenterX - int32(licenseKeyDisplayW/2)
p.licenseKeyDisplay.Y = currentY
currentY += int32(licenseKeyDisplayH) + licenseInfoMargin
}
// Email
if p.emailDisplay.ID != 0 {
emailDisplayDim := p.emailDisplay.RawDimensions()
emailDisplayW := emailDisplayDim[0]
p.emailDisplay.X = p.Base.ScreenCenterX - int32(emailDisplayW/2)
p.emailDisplay.Y = currentY
currentY += int32(emailDisplayH) + licenseInfoMargin
}
// Heading (top-most)
p.licenseInfoHeading.X = p.Base.ScreenCenterX - int32(licenseHeadingW/2)
p.licenseInfoHeading.Y = currentY
}
// 8) Get shader and render
textShader, ok := sm.Get("text")
if !ok {
return fmt.Errorf("failed to fetch text shader")
}
color := colors.White
p.Base.RenderTexture(textShader, p.title.ID, titlePos, titleDim, color)
p.Base.RenderTexture(textShader, p.version.ID, versionPos, versionDim, color)
p.Base.RenderTexture(textShader, p.prompt.ID, p.prompt.Position(), promptDim, color)
p.Base.RenderTexture(textShader, p.note.ID, p.note.Position(), p.note.RawDimensions(), color)
// Render display status
p.Base.RenderTexture(textShader, p.displayStatus.ID, p.displayStatus.Position(), p.displayStatus.RawDimensions(), color)
p.Base.RenderTexture(textShader, p.sdlDisplayStatus.ID, p.sdlDisplayStatus.Position(), p.sdlDisplayStatus.RawDimensions(), color)
// Render license info if it exists
if p.hasLicenseInfo {
p.Base.RenderTexture(textShader, p.licenseInfoHeading.ID, p.licenseInfoHeading.Position(), p.licenseInfoHeading.RawDimensions(), color)
if p.emailDisplay.ID != 0 {
p.Base.RenderTexture(textShader, p.emailDisplay.ID, p.emailDisplay.Position(), p.emailDisplay.RawDimensions(), color)
}
if p.licenseKeyDisplay.ID != 0 {
p.Base.RenderTexture(textShader, p.licenseKeyDisplay.ID, p.licenseKeyDisplay.Position(), p.licenseKeyDisplay.RawDimensions(), color)
}
}
return nil
}
// Destroy cleans up page-specific resources.
func (p *Welcome) Destroy() error {
gl.DeleteTextures(1, &p.title.ID)
gl.DeleteTextures(1, &p.version.ID)
gl.DeleteTextures(1, &p.prompt.ID)
gl.DeleteTextures(1, &p.note.ID)
gl.DeleteTextures(1, &p.displayStatus.ID)
gl.DeleteTextures(1, &p.sdlDisplayStatus.ID)
if p.hasLicenseInfo {
gl.DeleteTextures(1, &p.licenseInfoHeading.ID)
if p.emailDisplay.ID != 0 {
gl.DeleteTextures(1, &p.emailDisplay.ID)
}
if p.licenseKeyDisplay.ID != 0 {
gl.DeleteTextures(1, &p.licenseKeyDisplay.ID)
}
}
return p.Base.Destroy()
}
+790
View File
@@ -0,0 +1,790 @@
package page
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/godbus/dbus/v5"
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/keybinds"
"rhemawavetech.com/rhemawave/internal/network"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
// accessPointUIItem holds both the network data and its rendered texture.
type accessPointUIItem struct {
AP network.AccessPoint
Item StringItem
}
// WiFiSetupPage manages the discovery and connection to Wi-Fi networks.
type WiFiSetupPage struct {
Base
networkManager *network.Manager
applicationFont *fontManager.FontEntry
// Page-specific UI elements
titleItem StringItem
scanningStatusItem StringItem
connectionStatusItem StringItem
passwordPromptItem StringItem
passwordInputItem StringItem
passwordErrorItem StringItem
// Network list and selection
accessPoints []accessPointUIItem
selectedIndex int
scrollOffset int
// State management
isScanning bool
isEnteringPassword bool
isConnecting bool
connectionError string
passwordInput string
selectedAP *network.AccessPoint
wifiDevice *network.Device
connectionStatus *network.ConnectionStatus
activeSSID string // Tracks the CONFIRMED active network
pendingSSID string // Tracks the network we are TRYING to connect to
connectionAttemptTime time.Time
connectionTimeout time.Duration
// Text input deduplication
textInputSupported bool
// Async management
resultsChan chan []network.AccessPoint
statusChan chan *network.ConnectionStatus
errorChan chan error
stopScan chan struct{}
}
// Init sets up the page, initializes the network manager, and starts a scan.
func (p *WiFiSetupPage) Init(app ApplicationInterface) error {
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
// Get application font from database
applicationFontName, err := db.GetConfigValue("application_font")
if err != nil {
return fmt.Errorf("retrieving application_font from db: %w", err)
}
fm := fontManager.Get()
font, ok := fm.GetFont(applicationFontName)
if !ok {
return fmt.Errorf("could not find application font: %s", applicationFontName)
}
p.applicationFont = font
// Initialize network manager
nm, err := network.New()
if err != nil {
return fmt.Errorf("failed to initialize network manager: %w", err)
}
p.networkManager = nm
// Find Wi-Fi device
if err := p.networkManager.FindDevices(); err != nil {
}
for _, dev := range p.networkManager.Devices {
if dev.Type == network.TypeWifi {
d := dev
p.wifiDevice = &d
break
}
}
// Create UI elements
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
p.titleItem, err = NewStringItem("Wi-Fi Setup", p.applicationFont, white)
if err != nil {
return fmt.Errorf("failed to create title item: %w", err)
}
p.scanningStatusItem, err = NewStringItem("Scanning for networks...", p.applicationFont, white)
if err != nil {
return fmt.Errorf("failed to create scanning status item: %w", err)
}
// Initialize channels and timeout
p.resultsChan = make(chan []network.AccessPoint)
p.statusChan = make(chan *network.ConnectionStatus, 1)
p.errorChan = make(chan error)
p.stopScan = make(chan struct{})
p.connectionTimeout = 15 * time.Second
// Start scanning and status monitoring
p.startScan()
go p.checkConnectionStatusLoop()
return nil
}
// checkConnectionStatusLoop periodically checks the network status.
func (p *WiFiSetupPage) checkConnectionStatusLoop() {
ticker := time.NewTicker(2 * time.Second) // Check more frequently for better UX
defer ticker.Stop()
status, _ := p.networkManager.CheckInternetConnection()
p.statusChan <- status
for {
select {
case <-ticker.C:
status, _ := p.networkManager.CheckInternetConnection()
p.statusChan <- status
case <-p.stopScan:
return
}
}
}
// startScan triggers a new asynchronous network scan.
func (p *WiFiSetupPage) startScan() {
p.isScanning = true
p.selectedIndex = 0
p.scrollOffset = 0
p.accessPoints = nil
if p.wifiDevice != nil {
go p.networkManager.StartScan(p.resultsChan, p.errorChan, p.stopScan)
} else {
p.isScanning = false
}
}
// HandleEvent processes user input.
func (p *WiFiSetupPage) HandleEvent(event *sdl.Event) error {
if p.isEnteringPassword && !sdl.IsTextInputActive() {
p.enterPasswordMode()
}
switch e := (*event).(type) {
case *sdl.TextInputEvent:
if p.isEnteringPassword {
if !p.textInputSupported {
// First TextInputEvent: the keyboard fallback already
// handled this character, so just record that text input
// is supported and skip the duplicate append.
p.textInputSupported = true
} else {
p.passwordInput += e.GetText()
p.updatePasswordTexture()
}
}
case *sdl.KeyboardEvent:
if e.Type != sdl.KEYDOWN {
return nil
}
if p.isEnteringPassword {
p.handlePasswordInput(e.Keysym.Sym, e.Keysym.Mod)
} else {
p.handleNetworkSelection(e.Keysym.Sym)
}
}
return nil
}
// handlePasswordInput manages keyboard events during password entry.
func (p *WiFiSetupPage) handlePasswordInput(sym sdl.Keycode, mod uint16) {
switch sym {
case sdl.K_RETURN:
p.exitPasswordMode()
p.pendingSSID = p.selectedAP.SSID
p.isConnecting = true
p.connectionError = ""
p.connectionAttemptTime = time.Now()
err := p.networkManager.Connect(*p.wifiDevice, *p.selectedAP, p.passwordInput)
if err != nil {
p.isConnecting = false
p.pendingSSID = ""
p.connectionError = "Failed to initiate connection"
}
p.isEnteringPassword = false
case sdl.K_BACKSPACE:
if len(p.passwordInput) > 0 {
p.passwordInput = p.passwordInput[:len(p.passwordInput)-1]
p.updatePasswordTexture()
}
case sdl.K_ESCAPE:
p.isEnteringPassword = false
p.pendingSSID = ""
p.connectionError = ""
p.exitPasswordMode()
default:
// Keyboard fallback: if TextInputEvent was not received for this
// event cycle, convert printable keycodes to characters directly.
// This handles kiosk systems without an input method framework.
if !p.textInputSupported {
if ch := keycodeToChar(sym, mod); ch != 0 {
p.passwordInput += string(ch)
p.updatePasswordTexture()
}
}
}
}
// enterPasswordMode disables global keybinds and starts SDL text input.
func (p *WiFiSetupPage) enterPasswordMode() {
keybinds.Enabled = false
sdl.StartTextInput()
}
// exitPasswordMode re-enables global keybinds and stops SDL text input.
func (p *WiFiSetupPage) exitPasswordMode() {
keybinds.Enabled = true
sdl.StopTextInput()
}
// keycodeToChar converts an SDL keycode to a rune, respecting SHIFT state.
// Returns 0 for non-printable keys.
func keycodeToChar(sym sdl.Keycode, mod uint16) rune {
shifted := mod&sdl.KMOD_SHIFT != 0
// Letters
if sym >= sdl.K_a && sym <= sdl.K_z {
ch := rune(sym)
if shifted {
ch -= 'a' - 'A'
}
return ch
}
// Digits and their shifted symbols
if sym >= sdl.K_0 && sym <= sdl.K_9 {
if shifted {
shiftDigits := []rune{')', '!', '@', '#', '$', '%', '^', '&', '*', '('}
return shiftDigits[sym-sdl.K_0]
}
return rune(sym)
}
// Common punctuation / symbols
type charPair struct{ normal, shifted rune }
punctuation := map[sdl.Keycode]charPair{
sdl.K_SPACE: {' ', ' '},
sdl.K_MINUS: {'-', '_'},
sdl.K_EQUALS: {'=', '+'},
sdl.K_LEFTBRACKET: {'[', '{'},
sdl.K_RIGHTBRACKET: {']', '}'},
sdl.K_BACKSLASH: {'\\', '|'},
sdl.K_SEMICOLON: {';', ':'},
sdl.K_QUOTE: {'\'', '"'},
sdl.K_BACKQUOTE: {'`', '~'},
sdl.K_COMMA: {',', '<'},
sdl.K_PERIOD: {'.', '>'},
sdl.K_SLASH: {'/', '?'},
}
if pair, ok := punctuation[sym]; ok {
if shifted {
return pair.shifted
}
return pair.normal
}
return 0
}
// handleNetworkSelection manages keyboard events for the network list.
func (p *WiFiSetupPage) handleNetworkSelection(sym sdl.Keycode) {
switch sym {
case sdl.K_UP:
if p.selectedIndex > 0 {
p.selectedIndex--
}
case sdl.K_DOWN:
if p.selectedIndex < len(p.accessPoints)-1 {
p.selectedIndex++
}
case sdl.K_RETURN:
if len(p.accessPoints) > 0 && p.selectedIndex < len(p.accessPoints) {
p.selectedAP = &p.accessPoints[p.selectedIndex].AP
p.pendingSSID = p.selectedAP.SSID
p.connectionError = ""
if p.selectedAP.IsProtected {
p.isEnteringPassword = true
p.passwordInput = ""
p.updatePasswordTexture()
p.updatePasswordPromptTexture()
p.enterPasswordMode()
} else {
p.isConnecting = true
p.connectionAttemptTime = time.Now()
err := p.networkManager.Connect(*p.wifiDevice, *p.selectedAP, "")
if err != nil {
p.isConnecting = false
p.pendingSSID = ""
p.connectionError = "Failed to initiate connection"
}
}
}
case sdl.K_r:
if !p.isScanning {
p.startScan()
}
}
}
// Update checks for async results and updates UI state.
func (p *WiFiSetupPage) Update(dt float32) error {
select {
case results := <-p.resultsChan:
p.isScanning = false
p.buildNetworkUIList(results)
case status := <-p.statusChan:
p.connectionStatus = status
p.handleConnectionStatusUpdate(status)
p.updateConnectionStatusItem()
case err := <-p.errorChan:
p.isScanning = false
fmt.Printf("error scannign for networks %v", err)
default:
}
// Check for connection timeout
if p.isConnecting && time.Since(p.connectionAttemptTime) > p.connectionTimeout {
p.isConnecting = false
p.connectionError = "Connection timed out - Incorrect password?"
if p.selectedAP != nil && p.selectedAP.IsProtected {
p.isEnteringPassword = true
p.passwordInput = ""
p.updatePasswordTexture()
p.updatePasswordPromptTexture()
p.updatePasswordErrorTexture()
p.enterPasswordMode()
}
}
return nil
}
// handleConnectionStatusUpdate checks if connection succeeded or failed
func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionStatus) {
if !p.isConnecting {
return
}
// Check if we successfully connected to our pending SSID
switch status.StatusCode {
case network.StatusWifiConnectedInternetUp:
activeSSID := p.getActiveSSID()
if activeSSID == p.pendingSSID {
p.isConnecting = false
p.pendingSSID = ""
p.connectionError = ""
p.refreshNetworkColors()
}
case network.StatusWifiAvailableNotConnected:
// Connection failed - we're trying to connect but now show as not connected
if time.Since(p.connectionAttemptTime) > 5*time.Second {
p.isConnecting = false
if p.selectedAP != nil && p.selectedAP.IsProtected {
p.connectionError = "Connection failed - Incorrect password?"
p.isEnteringPassword = true
p.passwordInput = ""
p.updatePasswordTexture()
p.updatePasswordPromptTexture()
p.updatePasswordErrorTexture()
p.enterPasswordMode()
} else {
p.connectionError = "Connection failed"
p.pendingSSID = ""
}
}
}
}
// getActiveSSID queries NetworkManager for the currently active WiFi SSID
func (p *WiFiSetupPage) getActiveSSID() string {
if p.networkManager == nil || p.wifiDevice == nil {
return ""
}
nmObj := p.networkManager.Conn().Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
prop, err := nmObj.GetProperty("org.freedesktop.NetworkManager.ActiveConnections")
if err != nil {
return ""
}
if paths, ok := prop.Value().([]dbus.ObjectPath); ok {
for _, path := range paths {
acObj := p.networkManager.Conn().Object("org.freedesktop.NetworkManager", path)
devProp, err := acObj.GetProperty("org.freedesktop.NetworkManager.Connection.Active.Devices")
if err == nil {
if devPaths, ok := devProp.Value().([]dbus.ObjectPath); ok {
for _, devPath := range devPaths {
if string(devPath) == string(p.wifiDevice.Path) {
idProp, err := acObj.GetProperty("org.freedesktop.NetworkManager.Connection.Active.Id")
if err == nil {
if id, ok := idProp.Value().(string); ok {
return id
}
}
}
}
}
}
}
}
return ""
}
// refreshNetworkColors updates the colors of network items based on active status
func (p *WiFiSetupPage) refreshNetworkColors() {
activeSSID := p.getActiveSSID()
p.activeSSID = activeSSID
for i := range p.accessPoints {
if p.accessPoints[i].AP.SSID == activeSSID {
p.accessPoints[i].Item.Color = colors.LawnGreen
} else {
p.accessPoints[i].Item.Color = colors.White
}
}
}
// updateConnectionStatusItem creates the colored status message item.
func (p *WiFiSetupPage) updateConnectionStatusItem() {
if p.connectionStatusItem.ID != 0 {
gl.DeleteTextures(1, &p.connectionStatusItem.ID)
}
// Show connecting message if in progress
if p.isConnecting {
message := fmt.Sprintf("Connecting to %s...", p.pendingSSID)
item, err := NewStringItem(message, p.applicationFont, colors.Yellow)
if err == nil {
p.connectionStatusItem = item
}
return
}
// Show connection error if present
if p.connectionError != "" {
item, err := NewStringItem(p.connectionError, p.applicationFont, colors.Red)
if err == nil {
p.connectionStatusItem = item
}
return
}
if p.connectionStatus == nil {
return
}
switch p.connectionStatus.StatusCode {
case network.StatusWifiConnectedInternetUp:
message := fmt.Sprintf("Connected to %s - Internet accessible | F1 to exit", p.activeSSID)
item, err := NewStringItem(message, p.applicationFont, colors.LawnGreen)
if err == nil {
p.connectionStatusItem = item
}
p.refreshNetworkColors()
case network.StatusWifiAvailableNotConnected:
message := "WiFi available - Not connected | Select network or press R to rescan"
item, err := NewStringItem(message, p.applicationFont, colors.Yellow)
if err == nil {
p.connectionStatusItem = item
}
case network.StatusEthernetConnectedInternetUp:
message := "Ethernet connected - Internet accessible | F1 to exit"
item, err := NewStringItem(message, p.applicationFont, colors.LawnGreen)
if err == nil {
p.connectionStatusItem = item
}
case network.StatusEthernetNotConnected:
message := "No network connection | Select WiFi network or press R to rescan"
item, err := NewStringItem(message, p.applicationFont, colors.Red)
if err == nil {
p.connectionStatusItem = item
}
case network.StatusNoDevicesFound:
message := "No network devices found"
item, err := NewStringItem(message, p.applicationFont, colors.Red)
if err == nil {
p.connectionStatusItem = item
}
case network.StatusError:
message := "Network status check failed"
item, err := NewStringItem(message, p.applicationFont, colors.Red)
if err == nil {
p.connectionStatusItem = item
}
default:
item, err := NewStringItem(p.connectionStatus.Message, p.applicationFont, colors.White)
if err == nil {
p.connectionStatusItem = item
}
}
}
// buildNetworkUIList creates the renderable textures for the list of discovered networks.
func (p *WiFiSetupPage) buildNetworkUIList(aps []network.AccessPoint) {
// Get the currently active Wi-Fi SSID
activeSSID := p.getActiveSSID()
p.activeSSID = activeSSID
// Set IsActive flag based on the actual active SSID
for i := range aps {
aps[i].IsActive = (aps[i].SSID == p.activeSSID)
}
p.accessPoints = make([]accessPointUIItem, len(aps))
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
for i, ap := range aps {
ssid := ap.SSID
if ap.IsProtected {
ssid += " [L]"
}
color := white
if ap.IsActive {
color = colors.LawnGreen
}
item, err := NewStringItem(ssid, p.applicationFont, color)
if err != nil {
continue
}
p.accessPoints[i] = accessPointUIItem{AP: ap, Item: item}
}
}
// updatePasswordTexture regenerates the texture for the password input field.
func (p *WiFiSetupPage) updatePasswordTexture() {
if p.passwordInputItem.ID != 0 {
gl.DeleteTextures(1, &p.passwordInputItem.ID)
}
maskedPassword := strings.Repeat("*", len(p.passwordInput))
if maskedPassword == "" {
maskedPassword = " "
}
item, err := NewStringItem(maskedPassword, p.applicationFont, sdl.Color{R: 255, G: 255, B: 255, A: 255})
if err == nil {
p.passwordInputItem = item
}
}
// updatePasswordPromptTexture creates the texture for the password prompt label.
func (p *WiFiSetupPage) updatePasswordPromptTexture() {
if p.passwordPromptItem.ID != 0 {
gl.DeleteTextures(1, &p.passwordPromptItem.ID)
}
prompt := fmt.Sprintf("Password for %s:", p.selectedAP.SSID)
item, err := NewStringItem(prompt, p.applicationFont, sdl.Color{R: 255, G: 255, B: 255, A: 255})
if err == nil {
p.passwordPromptItem = item
}
}
// updatePasswordErrorTexture creates the texture for password error message.
func (p *WiFiSetupPage) updatePasswordErrorTexture() {
if p.passwordErrorItem.ID != 0 {
gl.DeleteTextures(1, &p.passwordErrorItem.ID)
}
if p.connectionError != "" {
item, err := NewStringItem(p.connectionError, p.applicationFont, colors.DarkRed)
if err == nil {
p.passwordErrorItem = item
}
}
}
// Render draws the entire page.
func (p *WiFiSetupPage) Render() error {
// Get base font size from database
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.ParseFloat(baseFontSizeStr, 32)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
// Colors & Config
bgColor := sdl.Color{R: 0, G: 0, B: 0, A: 255}
selectionColor := sdl.Color{R: 68, G: 71, B: 90, A: 255}
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
verticalMargin := float32(20)
solidShader, _ := shaderManager.Get().Get("solid_color")
textShader, _ := shaderManager.Get().Get("text")
// Background
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{0, 0}, mgl32.Vec2{float32(p.ScreenWidth), float32(p.ScreenHeight)}, bgColor)
// Title
titleScale := float32(48) / float32(baseFontSize)
titleW := float32(p.titleItem.W) * titleScale
titleH := float32(p.titleItem.H) * titleScale
titleX := float32(p.ScreenCenterX) - (titleW / 2)
titleY := float32(p.ScreenHeight) - titleH - 40
p.Base.RenderTexture(textShader, p.titleItem.ID, mgl32.Vec2{titleX, titleY}, mgl32.Vec2{titleW, titleH}, white)
// Status message
statusY := titleY - titleH - verticalMargin
if p.isScanning {
statusScale := float32(24) / float32(baseFontSize)
statusW := float32(p.scanningStatusItem.W) * statusScale
statusH := float32(p.scanningStatusItem.H) * statusScale
statusX := float32(p.ScreenCenterX) - (statusW / 2)
p.Base.RenderTexture(textShader, p.scanningStatusItem.ID, mgl32.Vec2{statusX, statusY}, mgl32.Vec2{statusW, statusH}, white)
} else if p.connectionStatusItem.ID != 0 {
statusScale := float32(24) / float32(baseFontSize)
statusW := float32(p.connectionStatusItem.W) * statusScale
statusH := float32(p.connectionStatusItem.H) * statusScale
statusX := float32(p.ScreenCenterX) - (statusW / 2)
p.Base.RenderTexture(textShader, p.connectionStatusItem.ID, mgl32.Vec2{statusX, statusY}, mgl32.Vec2{statusW, statusH}, p.connectionStatusItem.Color)
}
// Network list
listY := statusY - verticalMargin
if len(p.accessPoints) > 0 {
itemHeight := int32(40)
listPadding := int32(10)
for i, apItem := range p.accessPoints {
itemYpos := listY - float32(int32(i+1)*itemHeight)
itemXpos := float32(p.ScreenCenterX - 350)
itemW := float32(700)
if i == p.selectedIndex {
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{itemXpos, itemYpos}, mgl32.Vec2{itemW, float32(itemHeight)}, selectionColor)
}
textScale := float32(24) / float32(baseFontSize)
textW := float32(apItem.Item.W) * textScale
textH := float32(apItem.Item.H) * textScale
textY := itemYpos + (float32(itemHeight) / 2) - (textH / 2)
textX := itemXpos + float32(listPadding)
p.Base.RenderTexture(textShader, apItem.Item.ID, mgl32.Vec2{textX, textY}, mgl32.Vec2{textW, textH}, apItem.Item.Color)
}
}
// Password dialog overlay
if p.isEnteringPassword {
if err := p.renderPasswordDialog(solidShader, textShader, float32(baseFontSize)); err != nil {
return err
}
}
return nil
}
// renderPasswordDialog draws the password entry modal.
func (p *WiFiSetupPage) renderPasswordDialog(solidShader, textShader *shaderManager.Shader, baseFontSize float32) error {
overlayColor := sdl.Color{R: 0, G: 0, B: 0, A: 150}
dialogBgColor := sdl.Color{R: 40, G: 42, B: 54, A: 255}
boxBorderColor := sdl.Color{R: 120, G: 120, B: 120, A: 255}
boxBgColor := sdl.Color{R: 30, G: 30, B: 30, A: 255}
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
dialogW, dialogH := int32(600), int32(250)
if p.connectionError != "" {
dialogH = int32(300) // Make dialog taller if there's an error
}
dialogX, dialogY := p.ScreenCenterX-(dialogW/2), p.ScreenCenterY-(dialogH/2)
// Overlay and dialog background
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{0, 0}, mgl32.Vec2{float32(p.ScreenWidth), float32(p.ScreenHeight)}, overlayColor)
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{float32(dialogX), float32(dialogY)}, mgl32.Vec2{float32(dialogW), float32(dialogH)}, dialogBgColor)
currentY := float32(dialogY+dialogH) - 30
// Password error (if exists) - shown at the top in red
if p.passwordErrorItem.ID != 0 {
errorScale := float32(20) / baseFontSize
errorW := float32(p.passwordErrorItem.W) * errorScale
errorH := float32(p.passwordErrorItem.H) * errorScale
errorX := float32(p.ScreenCenterX) - (errorW / 2)
currentY -= errorH
p.Base.RenderTexture(textShader, p.passwordErrorItem.ID, mgl32.Vec2{errorX, currentY}, mgl32.Vec2{errorW, errorH}, colors.DarkRed)
currentY -= 20
}
// Password prompt
if p.passwordPromptItem.ID != 0 {
promptScale := float32(24) / baseFontSize
promptW := float32(p.passwordPromptItem.W) * promptScale
promptH := float32(p.passwordPromptItem.H) * promptScale
promptX := float32(p.ScreenCenterX) - (promptW / 2)
currentY -= promptH
p.Base.RenderTexture(textShader, p.passwordPromptItem.ID, mgl32.Vec2{promptX, currentY}, mgl32.Vec2{promptW, promptH}, white)
currentY -= 30
}
// Input box
padding, border := int32(4), int32(3)
boxW := int32(500)
targetFontSize := int32(36)
boxH := targetFontSize + (padding * 2)
borderBoxW, borderBoxH := boxW+(border*2), boxH+(border*2)
borderBoxX := p.ScreenCenterX - (borderBoxW / 2)
borderBoxY := int32(currentY) - boxH - (padding * 2)
boxX, boxY := p.ScreenCenterX-(boxW/2), int32(currentY)-boxH-padding
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{float32(borderBoxX), float32(borderBoxY)}, mgl32.Vec2{float32(borderBoxW), float32(borderBoxH)}, boxBorderColor)
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{float32(boxX), float32(boxY)}, mgl32.Vec2{float32(boxW), float32(boxH)}, boxBgColor)
// Password input text
if p.passwordInputItem.ID != 0 {
inputScale := float32(targetFontSize) / baseFontSize
inputW := float32(p.passwordInputItem.W) * inputScale
inputH := float32(p.passwordInputItem.H) * inputScale
inputX := float32(boxX + padding)
inputY := float32(boxY + padding)
p.Base.RenderTexture(textShader, p.passwordInputItem.ID, mgl32.Vec2{inputX, inputY}, mgl32.Vec2{inputW, inputH}, white)
}
return nil
}
// Destroy cleans up all resources used by the page.
func (p *WiFiSetupPage) Destroy() error {
// Re-enable global keybinds in case we're destroyed during password entry.
keybinds.Enabled = true
if p.stopScan != nil {
close(p.stopScan)
p.stopScan = nil
}
p.networkManager.Close()
gl.DeleteTextures(1, &p.titleItem.ID)
gl.DeleteTextures(1, &p.scanningStatusItem.ID)
if p.connectionStatusItem.ID != 0 {
gl.DeleteTextures(1, &p.connectionStatusItem.ID)
}
if p.passwordPromptItem.ID != 0 {
gl.DeleteTextures(1, &p.passwordPromptItem.ID)
}
if p.passwordInputItem.ID != 0 {
gl.DeleteTextures(1, &p.passwordInputItem.ID)
}
if p.passwordErrorItem.ID != 0 {
gl.DeleteTextures(1, &p.passwordErrorItem.ID)
}
for _, apItem := range p.accessPoints {
if apItem.Item.ID != 0 {
gl.DeleteTextures(1, &apItem.Item.ID)
}
}
return p.Base.Destroy()
}
+100
View File
@@ -0,0 +1,100 @@
package pattern
import (
"github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors"
)
type ColorPattern struct {
Colors []sdl.Color
colorIndex int
repeatsLeft int
a, b int
increasing bool
}
func NewColorPattern(colors []sdl.Color) *ColorPattern {
if len(colors) < 2 {
panic("ColorPattern requires at least 2 colors")
}
return &ColorPattern{
Colors: colors,
a: 0,
b: 1,
increasing: true,
repeatsLeft: 1, // Start with b (1)
}
}
// ATTENTION: TED
//
// # Fibonacci-Oscillating Temporal Color Distribution for Sequential Text Elements
//
// This method distributes intent-based colors across sequential text elements (words) using an
// oscillating Fibonacci sequence to control temporal color duration, creating rhythmic color
// patterns that maintain visual balance while varying exposure times.
//
// The technology addresses the challenge of applying multiple psychologically-selected colors
// to scrolling text in a way that maintains even distribution over time while creating natural
// rhythm. Conventional approaches either apply colors randomly (creating unbalanced temporal
// distribution) or use fixed intervals (producing mechanical, unnatural patterns that lack
// harmonic variation). Our method ensures each color in the palette appears throughout the
// entire display duration while varying its temporal presence according to natural mathematical
// progression.
//
// The approach uses a bidirectional Fibonacci sequence to determine how many consecutive words
// receive each color. The sequence increases (1, 1, 2, 3, 5, 8, 13, 21, 34, 55 words) then
// decreases back down (55, 34, 21, 13, 8, 5, 3, 2, 1, 1 words), creating an oscillating pattern.
// Colors from the intent-based palette are applied in sequence, with each color's duration
// determined by the current Fibonacci value. This creates a "breathing" rhythm where colors
// appear briefly, expand to longer durations, then contract back to brief appearances before
// cycling to the next color.
//
// Key differentiators from existing solutions:
// - Uses bidirectional Fibonacci sequence (ascending then descending) rather than fixed intervals
// - Creates natural rhythmic variation in color exposure duration
// - Ensures all palette colors appear throughout entire display timeline, not in isolated sections
// - Combines intent-based color selection with mathematical temporal distribution
// - Produces harmonic color rhythm synchronized with natural numerical progression
//
// Within our Rhema Wave generation system, this color distribution works in conjunction with
// the spatially-scrambled character patterns. While the scrambling creates spatial frequency
// signatures, the Fibonacci-oscillating color pattern creates temporal frequency modulation.
// The colors themselves are selected according to color theory principles based on the proclamation's
// psychological intent, and the oscillating Fibonacci distribution ensures these therapeutic colors
// maintain presence throughout the viewing session while creating harmonic temporal rhythm that
// reinforces the overall Rhema Wave effect.
func (c *ColorPattern) Color() sdl.Color {
// 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418
// This is a failsafe in case this pattern is used but colors are not supplied. Without this check, no text will display due to not having a color
// and becoming translucent.
if len(c.Colors) == 0 {
return colors.White // fallback to white
}
color := c.Colors[c.colorIndex%len(c.Colors)]
c.repeatsLeft--
if c.repeatsLeft <= 0 {
// Move to next color in the palette
c.colorIndex = (c.colorIndex + 1) % len(c.Colors)
// Update Fibonacci sequence for repeat count
if c.increasing {
c.a, c.b = c.b, c.a+c.b
if c.b > 55 { // Update this threahold to adjust the number of repeats
c.increasing = false
}
} else {
c.a, c.b = c.b-c.a, c.a
if c.a <= 0 {
c.a, c.b = 0, 1
c.increasing = true
}
}
c.repeatsLeft = c.b
}
return color
}
+127
View File
@@ -0,0 +1,127 @@
package pattern
type FibonacciPattern struct {
masterSequence []int
sequence []int
increasing bool
}
func init() {
Register("fibonacci", func() LinePattern {
return &FibonacciPattern{
masterSequence: []int{1, 2, 3, 5, 8, 13, 21},
sequence: []int{1},
increasing: true,
}
})
}
func (f *FibonacciPattern) Name() string { return "Fibonacci" }
// ATTENTION: TED
//
// # Fibonacci-Oscillating Spatial Distribution for Sequential Text Display
//
// This method arranges sequential text elements (words from proclamations) into display lines
// where the word count per line follows an oscillating Fibonacci sequence, creating both a
// visual wave pattern in two-dimensional space and ensuring even temporal distribution of
// content segments.
//
// The technology addresses the challenge of displaying repeated proclamation content in a way
// that maintains balanced exposure of all content portions while creating visually dynamic
// spatial patterns. Conventional scrolling text approaches either use fixed line lengths
// (creating uniform, non-dynamic displays) or random word counts (producing unbalanced content
// distribution where some portions appear more frequently than others). Our method ensures
// proclamation content is consumed in progressively sized segments that balance over time while
// creating a natural wave-like visual rhythm.
//
// The approach uses a bidirectional Fibonacci sequence to determine words per line. Starting
// with minimal word count (1, 1 words), the sequence expands (2, 3, 5, 8, 13, 21... words per
// line) until reaching a threshold, then contracts back down (21, 13, 8, 5, 3, 2, 1, 1 words),
// creating an oscillating wave. As lines scroll vertically, this varying horizontal word count
// creates a lateral wave pattern in the two-dimensional display space. Words are drawn sequentially
// from the proclamation text, and the varying segment sizes (1 word, then 2, then 3, then 5, etc.)
// ensure that over multiple oscillation cycles, the entire proclamation content receives balanced
// temporal exposure.
//
// Key differentiators from existing solutions:
// - Uses oscillating Fibonacci sequence for line word counts rather than fixed or random lengths
// - Creates two-dimensional wave pattern from one-dimensional text sequence
// - Ensures mathematical content distribution balance over time through progressive segmentation
// - Produces natural expansion-contraction rhythm synchronized with Fibonacci progression
// - Combines spatial wave formation with temporal content distribution in single mechanism
//
// Within our Rhema Wave generation system, this spatial distribution pattern works in concert
// with the character-level scrambling and color distribution methods. While character scrambling
// creates local spatial frequency patterns and color oscillation creates temporal color rhythm,
// the Fibonacci line length pattern generates a macro-level spatial wave that modulates the
// overall display geometry. The mathematical relationship between line lengths creates harmonic
// spatial progression, and the oscillating nature ensures proclamation content cycles through
// varying exposure durations, contributing to the layered frequency composition of the complete
// Rhema Wave output.
func (f *FibonacciPattern) GetNextLine(words []string, wordIndex *int, ctx *LineRenderContext) []string {
if ctx == nil || len(words) == 0 {
return nil
}
numWords := f.sequence[len(f.sequence)-1]
targetWidth := int32(ctx.ScreenWidth)
var line []string
var currentWidth int32
wordsAdded := 0
for wordsAdded < numWords && len(words) > 0 {
if int(*wordIndex) > len(words) {
*wordIndex = 0
}
word := words[*wordIndex%len(words)]
*wordIndex++
if word == "" {
continue
}
wordWidth := estimateWordWidth(word, ctx.Font, ctx.FontScale)
if len(line) > 0 {
wordWidth += ctx.SpaceWidth
}
if len(line) == 0 || currentWidth+wordWidth <= int32(float32(targetWidth)*float32(.9)) {
line = append(line, word)
currentWidth += wordWidth
wordsAdded++
} else {
break
}
}
// TODO: What should we do here instead?
// A line being longer than the display still needs to be handled, though, it should never happen
// if currentWidth >= ctx.ScreenWidth {
// syscall.Kill(syscall.Getpid(), syscall.SIGTRAP)
// }
// Update the sequence based on direction
if f.increasing {
if len(f.sequence) < len(f.masterSequence) {
// Grow sequence by appending next Fibonacci number
f.sequence = append(f.sequence, f.masterSequence[len(f.sequence)])
} else {
// Reached the top, now start shrinking
f.increasing = false
f.sequence = f.sequence[:len(f.sequence)-1]
}
} else {
if len(f.sequence) > 1 {
// Shrink the sequence
f.sequence = f.sequence[:len(f.sequence)-1]
} else {
// Hit bottom, start growing again
f.increasing = true
f.sequence = append(f.sequence, f.masterSequence[1]) // index 1 = second element = 2
}
}
return line
}
+71
View File
@@ -0,0 +1,71 @@
package pattern
import (
"fmt"
"rhemawavetech.com/rhemawave/internal/fontManager"
)
type LinePattern interface {
GetNextLine(words []string, wordIndex *int, ctx *LineRenderContext) []string // Generates the next line in the pattern
Name() string // Used to display the name of a LinePattern
}
// PatternInfo holds the public information about a registered pattern.
type PatternInfo struct {
Key string // The machine-readable key (e.g., "sine")
DisplayName string // The human-readable name for the UI (e.g., "Sine Wave")
}
type LineRenderContext struct {
ScreenWidth int32
Font *fontManager.FontEntry
FontScale float32
SpaceWidth int32
}
var Registry = make(map[string]func() LinePattern)
func Register(name string, constructor func() LinePattern) {
Registry[name] = constructor
}
func Get(name string) (LinePattern, error) {
constructor, ok := Registry[name]
if !ok {
return nil, fmt.Errorf("failed to find line pattern %s in Registry", name)
}
return constructor(), nil
}
// ListAvailable returns a slice of info for all registered patterns.
// This is used to build a settings menu.
func ListAvailable() []PatternInfo {
var list []PatternInfo
for key, constructor := range Registry {
pattern := constructor() // Create a temporary instance to get its name
list = append(list, PatternInfo{
Key: key,
DisplayName: pattern.Name(),
})
}
return list
}
// estimateWordWidth provides a basic estimate of the width of a word based on the font and scale.
func estimateWordWidth(word string, font *fontManager.FontEntry, scale float32) int32 {
if font == nil {
// Basic fallback estimate
return int32(float32(len(word)) * 10 * scale)
}
total := int32(0)
for _, ch := range word {
glyph, ok := fontManager.Get().GetGlyph(font.Name, ch)
if !ok {
continue
}
total += int32(float32(glyph.Advance) * scale)
}
return total
}
+80
View File
@@ -0,0 +1,80 @@
package pattern
import (
"math"
"rhemawavetech.com/rhemawave/internal/fontManager"
)
type SineWavePattern struct {
waveStepCounter int
}
func init() {
Register("sine", func() LinePattern { return &SineWavePattern{} })
}
func (s *SineWavePattern) Name() string { return "Sine Wave" }
func (s *SineWavePattern) GetNextLine(words []string, wordIndex *int, ctx *LineRenderContext) []string {
const (
totalLines = 67
frequency = 5.0 // cycles across all lines
amplitude = 0.85 // how much wave can scale
baseRatio = 0.03 // minimum line width as % of screen
sharpness = 3.0 // sharpness of the curve
)
screenWidth := int32(800)
spaceWidth := int32(10)
font := (*fontManager.FontEntry)(nil)
fontScale := float32(1.0)
if ctx != nil {
screenWidth = ctx.ScreenWidth
spaceWidth = ctx.SpaceWidth
font = ctx.Font
fontScale = ctx.FontScale
}
// Sharpen the wave (skinnier troughs)
// raw := (math.Sin(2*math.Pi*frequency*float64(s.waveStepCounter)/totalLines) + 1.0) / 2.0
// waveVal := math.Pow(raw, sharpness)
angle := 2 * math.Pi * frequency * float64(s.waveStepCounter) / totalLines
waveVal := (math.Sin(angle) + 1.0) / 2.0
// Target width in pixels
targetWidth := int32(float64(screenWidth) * (baseRatio + amplitude*waveVal))
var line []string
var currentWidth int32
for len(words) > 0 {
word := words[*wordIndex%len(words)]
*wordIndex++
if word == "" {
continue
}
wordWidth := estimateWordWidth(word, font, fontScale)
if len(line) > 0 {
wordWidth += spaceWidth
}
if len(line) == 0 {
// Always include the first word
line = append(line, word)
currentWidth += wordWidth
} else {
// Only add next word if it fits
if currentWidth+wordWidth > targetWidth {
break
}
line = append(line, word)
currentWidth += wordWidth
}
}
s.waveStepCounter++
return line
}
+206
View File
@@ -0,0 +1,206 @@
package shaderManager
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/go-gl/gl/v3.3-core/gl"
"rhemawavetech.com/rhemawave/internal/db"
)
// Shader represents a compiled GLSL program and its associated data.
type Shader struct {
Name string
ProgramID uint32
Render func() // This will be used in the future.
// Cached uniform locations (looked up once after linking)
LocMVPMatrix int32
LocTexture int32
LocTextColor int32
LocColor int32
}
// ShaderManager handles the loading, compiling, and management of GLSL shader programs.
type ShaderManager struct {
shaders map[string]*Shader
}
var (
instance *ShaderManager
once sync.Once
)
// InitShaderManager initializes the ShaderManager singleton.
func InitShaderManager() error {
var err error
once.Do(func() {
sm := &ShaderManager{
shaders: make(map[string]*Shader),
}
if e := sm.LoadShaders(); e != nil {
err = fmt.Errorf("could not load shaders: %w", e)
return
}
instance = sm
})
return err
}
// Get returns the singleton ShaderManager instance.
func Get() *ShaderManager {
if instance == nil {
panic("ShaderManager has not been initialized. Call InitShaderManager at application startup.")
}
return instance
}
// Get retrieves a compiled Shader object by its name.
func (sm *ShaderManager) Get(name string) (*Shader, bool) {
shader, ok := sm.shaders[name]
return shader, ok
}
// ListShaders returns all shaders.
func (sm *ShaderManager) ListShaders() map[string]*Shader {
return sm.shaders
}
// Close deletes all loaded shader programs from the GPU to free up resources.
func (sm *ShaderManager) Close() {
for name, shader := range sm.shaders {
gl.DeleteProgram(shader.ProgramID)
delete(sm.shaders, name)
}
}
// LoadShaders scans a directory for .vert and .frag files and creates a shader program for each pair.
func (sm *ShaderManager) LoadShaders() error {
// Get assets directory from database
assetsDir, err := db.GetConfigValue("assets_dir")
if err != nil {
return fmt.Errorf("retrieving assets_dir from db: %w", err)
}
srcDir := filepath.Join(assetsDir, "shaders")
shaderFiles := make(map[string]string)
err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".vert") {
name := strings.TrimSuffix(info.Name(), ".vert")
shaderFiles[name] = path
}
return nil
})
if err != nil {
return fmt.Errorf("error walking shader directory: %w", err)
}
for name, vertPath := range shaderFiles {
fragPath := strings.Replace(vertPath, ".vert", ".frag", 1)
if _, err := os.Stat(fragPath); os.IsNotExist(err) {
continue
}
programID, err := createShaderProgram(vertPath, fragPath)
if err != nil {
continue
}
newShader := &Shader{
Name: name,
ProgramID: programID,
Render: nil, // Render function is not yet assigned.
}
newShader.cacheUniformLocations()
sm.shaders[name] = newShader
}
return nil
}
// cacheUniformLocations looks up common uniform locations once and stores them on the Shader.
func (s *Shader) cacheUniformLocations() {
s.LocMVPMatrix = gl.GetUniformLocation(s.ProgramID, gl.Str("u_mvpMatrix\x00"))
s.LocTexture = gl.GetUniformLocation(s.ProgramID, gl.Str("u_texture\x00"))
s.LocTextColor = gl.GetUniformLocation(s.ProgramID, gl.Str("u_textColor\x00"))
s.LocColor = gl.GetUniformLocation(s.ProgramID, gl.Str("u_color\x00"))
}
// createShaderProgram reads, compiles, and links a vertex and fragment shader from file paths.
func createShaderProgram(vertexPath, fragmentPath string) (uint32, error) {
vertexSource, err := os.ReadFile(vertexPath)
if err != nil {
return 0, fmt.Errorf("reading vertex shader %s: %w", vertexPath, err)
}
fragmentSource, err := os.ReadFile(fragmentPath)
if err != nil {
return 0, fmt.Errorf("reading fragment shader %s: %w", fragmentPath, err)
}
vertexShader, err := compileShader(string(vertexSource)+"\x00", gl.VERTEX_SHADER)
if err != nil {
return 0, err
}
defer gl.DeleteShader(vertexShader)
fragmentShader, err := compileShader(string(fragmentSource)+"\x00", gl.FRAGMENT_SHADER)
if err != nil {
return 0, err
}
defer gl.DeleteShader(fragmentShader)
program := gl.CreateProgram()
gl.AttachShader(program, vertexShader)
gl.AttachShader(program, fragmentShader)
gl.LinkProgram(program)
var status int32
gl.GetProgramiv(program, gl.LINK_STATUS, &status)
if status == gl.FALSE {
var logLength int32
gl.GetProgramiv(program, gl.INFO_LOG_LENGTH, &logLength)
log := strings.Repeat("\x00", int(logLength+1))
gl.GetProgramInfoLog(program, logLength, nil, gl.Str(log))
return 0, fmt.Errorf("failed to link program: %v", log)
}
return program, nil
}
// compileShader is a helper function that compiles a single shader source string.
func compileShader(source string, shaderType uint32) (uint32, error) {
shader := gl.CreateShader(shaderType)
csources, free := gl.Strs(source)
gl.ShaderSource(shader, 1, csources, nil)
free()
gl.CompileShader(shader)
var status int32
gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
if status == gl.FALSE {
var logLength int32
gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
log := strings.Repeat("\x00", int(logLength+1))
gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))
var shaderTypeName string
switch shaderType {
case gl.VERTEX_SHADER:
shaderTypeName = "Vertex"
case gl.FRAGMENT_SHADER:
shaderTypeName = "Fragment"
default:
shaderTypeName = "Unknown"
}
return 0, fmt.Errorf("failed to compile %s shader: %v", shaderTypeName, log)
}
return shader, nil
}
+796
View File
@@ -0,0 +1,796 @@
# 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.<system>.default` | The RhemaWave binary |
| `devShells.<system>.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 <<EOF
StoreDir: /nix/store
WantMassQuery: 1
Priority: 40
EOF
```
Then configure Caddy (see section 8).
---
## 8. Caddy Server Configuration
The web server at `repository.rhemawavetech.com` runs Caddy with a `file_server` directive pointing at the cache directory.
### Caddyfile
```caddyfile
repository.rhemawavetech.com {
root * /var/lib/nix-cache
file_server browse
}
```
The `browse` directive is optional (useful for debugging). Caddy handles TLS automatically via Let's Encrypt.
### Verification
```bash
curl https://repository.rhemawavetech.com/nix-cache-info
```
Should return the three-line metadata file.
---
## 9. Signing Keys
### How Signing Works
1. The developer builds a package with `nix build`
2. The store path is signed with the private key: `nix store sign --key-file ...`
3. The signed package is exported to cache format: `nix copy --to file:// ...`
4. The `.narinfo` file contains a `Sig:` line with the key name and signature
5. Client machines verify the signature against the public key in `trusted-public-keys`
### Key Name
The key name `repository.rhemawavetech.com-1` is embedded in both the key files and the `.narinfo` signatures. If you generate new keys, use the same naming convention with an incremented number (e.g., `-2`).
### Generating New Keys
```bash
nix key generate-secret --key-name repository.rhemawavetech.com-1 > 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/<name>`
### .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`
+83
View File
@@ -0,0 +1,83 @@
self:
{ config, lib, pkgs, ... }:
let
cfg = config.services.rhemawave;
in
{
options.services.rhemawave = {
enable = lib.mkEnableOption "RhemaWave kiosk application";
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.system}.default;
description = "The RhemaWave package to use.";
};
# There should not be a "kioskUser" defined in the program
# That should be defined by the system, so everything kioskUser related
# has been commented out.
user = lib.mkOption {
type = lib.types.str;
default = "kiosk";
description = "User account that runs the kiosk session.";
};
# extraGroups = lib.mkOption {
# type = lib.types.listOf lib.types.str;
# default = [];
# description = "Additional groups for the kiosk user.";
# };
autoVolume = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Set all PipeWire sinks to full volume before launching.";
};
};
config = lib.mkIf cfg.enable {
# # --- Kiosk user ---
# users.users.${cfg.kioskUser} = {
# isNormalUser = true;
# group = cfg.kioskUser;
# extraGroups = [ "audio" "video" "input" "networkmanager" ] ++ cfg.extraGroups;
# };
# users.groups.${cfg.kioskUser} = {};
# --- Display: agetty + cage ---
# --- Environment ---
environment.variables.SDL_VIDEODRIVER = "wayland";
# --- Networking ---
networking.networkmanager.enable = true;
# --- Audio: PipeWire + WirePlumber ---
services.pipewire = {
enable = true;
alsa.enable = true;
pulse.enable = true;
wireplumber.enable = true;
};
# --- Polkit rules ---
security.polkit.extraConfig = ''
polkit.addRule(function(action, subject) {
// Allow restarting getty@tty1
if (action.id == "org.freedesktop.systemd1.manage-units") {
var unit = action.lookup("unit");
if (unit == "getty@tty1.service") {
return polkit.Result.YES;
}
}
});
'';
# --- Make the package available system-wide ---
environment.systemPackages = [ cfg.package ];
};
}
@@ -0,0 +1,55 @@
// Allow kiosk user to run apt update, apt install for rhemawave packages,
// and restart getty for application reloading
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.policykit.exec" &&
subject.user == "kiosk") {
var program = action.lookup("program");
var cmdline = action.lookup("command_line");
// Allow dpkg --configure -a
if (program == "/usr/bin/dpkg") {
if (cmdline && cmdline.indexOf("--configure") >= 0) {
return polkit.Result.YES;
}
}
// Allow apt update
if (program == "/usr/bin/apt") {
if (cmdline && cmdline.indexOf("update") >= 0) {
return polkit.Result.YES;
}
// Allow: apt install --only-upgrade rhemawave[-dev]
if (cmdline &&
cmdline.indexOf("install") >= 0 &&
cmdline.indexOf("--only-upgrade") >= 0 &&
cmdline.indexOf("rhemawave") >= 0) {
return polkit.Result.YES;
}
}
// Allow systemctl restart getty@tty1.service
if (program == "/usr/bin/systemctl") {
if (cmdline &&
cmdline.indexOf("restart") >= 0 &&
cmdline.indexOf("getty@tty1.service") >= 0) {
return polkit.Result.YES;
}
}
// Allow sh for Tailscale install script
if (program == "/usr/bin/sh" || program == "/bin/sh") {
if (cmdline && cmdline.indexOf("tailscale.com/install.sh") >= 0) {
return polkit.Result.YES;
}
}
// Allow tailscale set --operator
if (program == "/usr/bin/tailscale") {
if (cmdline && cmdline.indexOf("set") >= 0 && cmdline.indexOf("--operator") >= 0) {
return polkit.Result.YES;
}
}
}
});
+3
View File
@@ -0,0 +1,3 @@
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin kiosk --noclear %I $TERM
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# Post-installation script for RhemaWave
set -e
# Create kiosk group if it doesn't exist
if ! getent group kiosk > /dev/null 2>&1; then
echo "Creating kiosk group..."
groupadd --system kiosk
fi
# Create kiosk user if it doesn't exist
if ! getent passwd kiosk > /dev/null 2>&1; then
echo "Creating kiosk user..."
useradd --system --gid kiosk --create-home --home-dir /home/kiosk \
--shell /bin/bash --comment "RhemaWave Kiosk User" kiosk
fi
# Set proper group ownership for writable directories
chgrp -R kiosk /usr/local/share/rhemawave
# Ensure kiosk user can write to the data directory
chmod g+w /usr/local/share/rhemawave
echo "✓ RhemaWave post-install complete"
+16
View File
@@ -0,0 +1,16 @@
# RhemaWave kiosk auto-launch on tty1
if [ -z "$WAYLAND_DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export SDL_VIDEODRIVER=wayland
# Check for updates before launching
/usr/bin/python3 /usr/local/bin/rhemawave-updater.py || true
# Set all sinks to full volume
for sink in $(wpctl status | grep -oP '^\s+\K\d+(?=\.)' | head -10); do
wpctl set-volume "$sink" 1.0 2>/dev/null
done
exec cage -- /usr/local/bin/rhemawave
fi
+564
View File
@@ -0,0 +1,564 @@
#!/usr/bin/env python3
"""
rhemawave-updater.py
Auto-updater for RhemaWave kiosk application.
Uses apt to check for and install updates from the configured repository.
USAGE:
Run as kiosk user:
python3 /usr/local/bin/rhemawave-updater.py
"""
import sys
import subprocess
import json
import socket
import urllib.request
from pathlib import Path
from datetime import datetime
# Configuration - templated by build script
PACKAGE_NAME = "__CHANNEL__"
HEADSCALE_URL = "https://headscale.rhemawavetech.com"
HEADSCALE_AUTHKEY = "803130d5b2e38bffa9fa5b6921a3d6403100e2e30fea1bd5"
# Paths
LOG_FILE = Path("/home/kiosk/rhemawave-updater.log")
LICENSE_KEY_PATH = Path.home() / ".local/share/rhemawave/license_key"
class UpdaterLogger:
"""Simple logger that writes to both stdout and file"""
def __init__(self, log_file):
self.log_file = log_file
log_file.parent.mkdir(parents=True, exist_ok=True)
log_file.touch(exist_ok=True)
def log(self, message):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
formatted = f"[{timestamp}] {message}"
print(formatted)
try:
with open(self.log_file, 'a') as f:
f.write(formatted + '\n')
except IOError:
pass
def error(self, message):
self.log(f"ERROR: {message}")
def debug(self, message):
self.log(f"DEBUG: {message}")
def success(self, message):
self.log(f"✓ {message}")
logger = UpdaterLogger(LOG_FILE)
def check_network(timeout=2):
"""Quick check for internet connectivity."""
logger.log("Checking network connectivity...")
try:
socket.create_connection(("1.1.1.1", 53), timeout=timeout)
logger.success("Network check passed (1.1.1.1:53 reachable)")
return True
except OSError as e:
logger.error(f"Network check failed: {e}")
return False
def run_command(cmd, description, use_pkexec=False):
"""Run a command and return success status"""
if use_pkexec:
cmd = ["pkexec"] + cmd
logger.log(f"{description}...")
logger.debug(f"Command: {' '.join(cmd)}")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=300
)
logger.debug(f"Return code: {result.returncode}")
if result.stdout:
for line in result.stdout.splitlines():
logger.log(f" {line}")
if result.returncode != 0:
logger.error(f"{description} failed with return code {result.returncode}")
if result.stderr:
for line in result.stderr.splitlines():
logger.error(f" {line}")
return False
logger.success(f"{description} completed successfully")
return True
except subprocess.TimeoutExpired:
logger.error(f"{description} timed out")
return False
except Exception as e:
logger.error(f"{description} failed: {e}")
return False
def configure_dpkg():
logger.log("Checking for dpkg errors")
if not run_command(
["dpkg", "--configure", "-a"],
'Check for dpkg configuration errors and resolve them before performing updates',
use_pkexec=True):
logger.error("Unable to reconfigure system, please reach out to support!")
return
def is_tailscale_installed():
"""Check if Tailscale is installed."""
logger.log("Checking if Tailscale is installed...")
try:
result = subprocess.run(
["tailscale", "version"],
capture_output=True,
text=True,
check=True
)
version = result.stdout.strip()
logger.success(f"Tailscale is installed: {version}")
return True
except FileNotFoundError:
logger.log("Tailscale binary not found")
return False
except subprocess.CalledProcessError as e:
logger.error(f"Tailscale version check failed: {e}")
return False
def install_tailscale():
"""Install Tailscale."""
logger.log("Installing Tailscale...")
try:
logger.debug("Running: pkexec sh -c 'curl -fsSL https://tailscale.com/install.sh | sh'")
result = subprocess.run(
["pkexec", "sh", "-c", "curl -fsSL https://tailscale.com/install.sh | sh"],
capture_output=True,
text=True,
timeout=120
)
logger.debug(f"Install return code: {result.returncode}")
if result.stdout:
for line in result.stdout.splitlines():
logger.log(f" {line}")
if result.stderr:
for line in result.stderr.splitlines():
logger.log(f" [stderr] {line}")
if result.returncode != 0:
logger.error("Tailscale installation failed")
return False
logger.success("Tailscale installed successfully")
return True
except subprocess.TimeoutExpired:
logger.error("Tailscale installation timed out")
return False
except subprocess.CalledProcessError as e:
logger.error(f"Tailscale installation failed: {e}")
if e.stderr:
logger.error(f"stderr: {e.stderr}")
return False
def set_tailscale_operator():
"""Set Tailscale operator to kiosk."""
logger.log("Setting Tailscale operator to 'kiosk'...")
try:
result = subprocess.run(
["pkexec", "tailscale", "set", "--operator=kiosk"],
capture_output=True,
text=True,
timeout=30
)
logger.debug(f"Return code: {result.returncode}")
if result.stdout:
logger.debug(f"stdout: {result.stdout}")
if result.stderr:
logger.debug(f"stderr: {result.stderr}")
if result.returncode != 0:
logger.error(f"Failed to set operator: {result.stderr}")
return False
logger.success("Tailscale operator set to 'kiosk'")
return True
except Exception as e:
logger.error(f"Failed to set Tailscale operator: {e}")
return False
def get_tailscale_status():
"""Get full Tailscale status as dict."""
logger.log("Getting Tailscale status...")
try:
result = subprocess.run(
["tailscale", "status", "--json"],
capture_output=True,
text=True,
timeout=10
)
logger.debug(f"Return code: {result.returncode}")
if result.returncode != 0:
logger.error(f"tailscale status failed: {result.stderr}")
return None
status = json.loads(result.stdout)
# Log key status info
backend_state = status.get("BackendState", "unknown")
logger.log(f" BackendState: {backend_state}")
self_info = status.get("Self", {})
if self_info:
logger.log(f" HostName: {self_info.get('HostName', 'unknown')}")
logger.log(f" DNSName: {self_info.get('DNSName', 'unknown')}")
logger.log(f" Online: {self_info.get('Online', 'unknown')}")
tailscale_ips = self_info.get("TailscaleIPs", [])
logger.log(f" TailscaleIPs: {tailscale_ips}")
# Check for health issues
health = status.get("Health", [])
if health:
logger.log(" Health issues:")
for issue in health:
logger.log(f" - {issue}")
return status
except json.JSONDecodeError as e:
logger.error(f"Failed to parse Tailscale status JSON: {e}")
return None
except subprocess.TimeoutExpired:
logger.error("Tailscale status timed out")
return None
except Exception as e:
logger.error(f"Failed to get Tailscale status: {e}")
return None
def is_tailscale_logged_in(status=None):
"""Check if Tailscale is logged in."""
if status is None:
status = get_tailscale_status()
if status is None:
logger.error("Cannot determine login status - no status available")
return False
backend_state = status.get("BackendState", "")
logged_in = backend_state == "Running"
if logged_in:
logger.success("Tailscale is logged in (BackendState=Running)")
else:
logger.log(f"Tailscale is NOT logged in (BackendState={backend_state})")
return logged_in
def tailscale_login(hostname=None):
"""Log in to Tailscale/Headscale."""
logger.log("Logging in to Headscale...")
logger.debug(f" Server: {HEADSCALE_URL}")
logger.debug(f" AuthKey: {HEADSCALE_AUTHKEY[:8]}...{HEADSCALE_AUTHKEY[-4:]}")
if hostname:
logger.debug(f" Hostname: {hostname}")
cmd = [
"tailscale", "up",
"--login-server", HEADSCALE_URL,
"--authkey", HEADSCALE_AUTHKEY,
"--force-reauth"
]
if hostname:
cmd.extend(["--hostname", hostname])
logger.debug(f"Command: {' '.join(cmd)}")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
logger.debug(f"Return code: {result.returncode}")
if result.stdout:
logger.log(f" stdout: {result.stdout}")
if result.stderr:
logger.log(f" stderr: {result.stderr}")
if result.returncode != 0:
logger.error(f"Tailscale login failed with code {result.returncode}")
return False
logger.success("Tailscale login successful")
return True
except subprocess.TimeoutExpired:
logger.error("Tailscale login timed out")
return False
except Exception as e:
logger.error(f"Tailscale login failed: {e}")
return False
def get_current_tailscale_hostname(status=None):
"""Get the hostname we're currently advertising."""
if status is None:
status = get_tailscale_status()
if status is None:
return None
hostname = status.get("Self", {}).get("HostName")
logger.debug(f"Current Tailscale hostname: {hostname}")
return hostname
def get_license_key():
"""Read license key from plain text file."""
logger.log("Fetching license key...")
logger.debug(f" Path: {LICENSE_KEY_PATH}")
if not LICENSE_KEY_PATH.exists():
logger.log(" License key file does not exist")
return None
try:
license_key = LICENSE_KEY_PATH.read_text().strip()
if not license_key:
logger.log(" License key file is empty")
return None
logger.success(f"License key found: {license_key}")
return license_key
except Exception as e:
logger.error(f"Failed to read license key: {e}")
return None
def ensure_tailscale_installed():
"""Ensure Tailscale is installed, install if needed."""
logger.log("=== Ensuring Tailscale is installed ===")
if is_tailscale_installed():
return True
logger.log("Tailscale not installed, attempting installation...")
if not install_tailscale():
return False
# Verify installation
if not is_tailscale_installed():
logger.error("Tailscale installation verification failed")
return False
# Set operator
if not set_tailscale_operator():
logger.error("Failed to set Tailscale operator, continuing anyway...")
return True
def ensure_tailscale_connected():
"""Ensure Tailscale is logged in and connected."""
logger.log("=== Ensuring Tailscale is connected ===")
status = get_tailscale_status()
if not is_tailscale_logged_in(status):
logger.log("Tailscale not logged in, attempting login...")
# Get license key to use as hostname if available
license_key = get_license_key()
if not tailscale_login(hostname=license_key):
logger.error("Failed to log in to Tailscale")
return False
# Re-check status after login
status = get_tailscale_status()
if not is_tailscale_logged_in(status):
logger.error("Still not logged in after login attempt")
return False
logger.success("Tailscale is connected")
return True
def sync_tailscale_hostname():
"""Update tailscale hostname to match license key if needed."""
logger.log("=== Syncing Tailscale hostname ===")
license_key = get_license_key()
if not license_key:
logger.log("No license key found, skipping hostname sync")
return
status = get_tailscale_status()
current = get_current_tailscale_hostname(status)
logger.log(f" Current hostname: {current}")
logger.log(f" Desired hostname: {license_key}")
if current == license_key:
logger.success(f"Hostname already correct: {license_key}")
return
logger.log(f"Hostname mismatch, updating: {current} -> {license_key}")
if tailscale_login(hostname=license_key):
logger.success("Hostname updated successfully")
# Verify the change
new_status = get_tailscale_status()
new_hostname = get_current_tailscale_hostname(new_status)
logger.log(f" Verified new hostname: {new_hostname}")
else:
logger.error("Failed to update hostname")
def sync_ssh_keys():
"""Fetch SSH public keys from GitHub and write to authorized_keys."""
logger.log("=== Syncing SSH keys from GitHub ===")
github_user = "nclack"
url = f"https://github.com/{github_user}.keys"
try:
logger.debug(f"Fetching keys from {url}")
req = urllib.request.Request(url, headers={"User-Agent": "rhemawave-updater"})
with urllib.request.urlopen(req, timeout=10) as resp:
keys = resp.read().decode().strip()
if not keys:
logger.error("No SSH keys returned from GitHub")
return
ssh_dir = Path.home() / ".ssh"
ssh_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
auth_keys = ssh_dir / "authorized_keys"
auth_keys.write_text(keys + "\n")
auth_keys.chmod(0o600)
key_count = len(keys.splitlines())
logger.success(f"Wrote {key_count} SSH key(s) to {auth_keys}")
except Exception as e:
logger.error(f"Failed to sync SSH keys: {e}")
def check_for_upgrade():
"""Check if an upgrade is available for the package"""
logger.log(f"Checking for upgrades to {PACKAGE_NAME}...")
try:
result = subprocess.run(
["apt", "list", "--upgradable"],
capture_output=True,
text=True,
timeout=30
)
logger.debug(f"apt list output: {result.stdout}")
if PACKAGE_NAME in result.stdout:
logger.success(f"Upgrade available for {PACKAGE_NAME}")
return True
else:
logger.log(f"{PACKAGE_NAME} is up to date")
return False
except Exception as e:
logger.error(f"Could not check for upgrades: {e}")
return False
def main():
logger.log("=========================================")
logger.log("RhemaWave Updater")
logger.log(f"Package: {PACKAGE_NAME}")
logger.log(f"Timestamp: {datetime.now().isoformat()}")
logger.log("=========================================")
# Quick network check
if not check_network():
logger.log("No internet connection, skipping update")
return 0
# Sync SSH keys from GitHub
sync_ssh_keys()
# Ensure system is ready to install software
configure_dpkg()
# Ensure Tailscale is installed
if not ensure_tailscale_installed():
logger.error("Tailscale installation failed, continuing with update check...")
else:
# Ensure Tailscale is connected
if not ensure_tailscale_connected():
logger.error("Tailscale connection failed, continuing with update check...")
else:
# Sync hostname with license key
sync_tailscale_hostname()
logger.log("=========================================")
logger.log("=== Package Update Check ===")
logger.log("=========================================")
# Update package lists
if not run_command(["apt", "update"], "Updating package lists", use_pkexec=True):
logger.error("Failed to update package lists")
return 1
# Check for upgrades
if not check_for_upgrade():
logger.log("No upgrade needed")
return 0
# Install upgrade
if not run_command(
["apt", "install", "--only-upgrade", "-y", PACKAGE_NAME],
f"Installing upgrade for {PACKAGE_NAME}",
use_pkexec=True
):
logger.error("Upgrade installation failed")
return 1
logger.success("Upgrade installed successfully!")
# Restart getty to reload the application
if not run_command(
["systemctl", "restart", "getty@tty1.service"],
"Restarting application",
use_pkexec=True
):
logger.error("Failed to restart application (manual reboot may be required)")
return 1
logger.success("Application restarted")
logger.log("=========================================")
logger.log("=== Update Complete ===")
logger.log("=========================================")
return 0
if __name__ == "__main__":
sys.exit(main())
+12
View File
@@ -0,0 +1,12 @@
package main
import (
"fmt"
"rhemawavetech.com/rhemawave/internal/application"
)
func main() {
application.Run()
fmt.Printf("Stopping application")
}
+64
View File
@@ -0,0 +1,64 @@
#!/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 ""
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and blesses all aspects of my life.
I am thankful for my health, vitality, and motivation to pursue a deeper
understanding of the Trinity so that I become a shining example of
someone who’s life is truly blessed and lived according to GOD’s design.
I appreciate GOD providing strength and direction to every cell in my
body and blesses my life in countless ways.
I am pleased I have the freedom to worship our creator, follow his
teachings, and receive blessings according to GOD’s design.
I enjoy knowing GOD protects me, blesses me, and allows me to pursue a
deep and personal relationship with him.
I am happy I can pray, speak with GOD directly, seek his advice, follow
his teachings, and receive his blessings.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life.
I am thankful I wake up each morning inspired to learn, trust, and follow
GOD’s plan for my life.
I appreciate GOD’s almighty power flowing though me and creating
miracles in all areas of my life.
I am pleased my life is blessed and I live a perpetually blissful life.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful to GOD for his forgiveness, understanding, and love.
@@ -0,0 +1,56 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to see the best in
every person and every opportunity.
I am thankful for my life, health, and vitality and express my
appreciation by always focusing on the positive aspects of every
situation I encounter.
I appreciate GOD providing strength and direction to every cell in my
body which I use to maintain a positive attitude regardless of my
personal situation.
I am pleased I always think positive, manage my thoughts, and live each
day striving energetically to accomplish goals which positively effect
my life and the lives of those around me according to GOD’s design.
I enjoy knowing GOD protects me and allows me to achieve things I once
thought impossible.
I am happy I think positively, I do not dwell on negative issues, and I
control my thoughts at all times.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life.
I am thankful I wake up each morning yearning to efficiently accomplish
as many tasks as possible to ensure success in all my endeavors.
I appreciate GOD’s almighty power flowing through me and creating
miracles in all areas of my life as a reward my optimism.
I am pleased my mental health and emotional stability allow me remain
positive and live a relaxed and perpetually blissful life.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful that I only seek the good in myself, the good in others,
the good in the world around me, and the greatness of the live I am
living.
@@ -0,0 +1,56 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and causes my circulatory system to
remain clear and unobstructed for for optimum health and longevity.
I am thankful for my health, vitality, and motivation to eat properly,
exercise, and strengthen my body.
I appreciate GOD providing strength and direction to every cell in my
body ensuring my blood chemistry is, and remains optimal for excellent
health and longevity.
I am pleased my muscles, bones, joints, organs, nerves, tendons, and
ligaments all develop correctly and function normally.
I enjoy knowing GOD protects me throughout and beyond the physical
healing and development process.
I am happy my HDL, LDL, Triglycerides, and Blood Sugar levels are and
remain at ideal levels for my optimal health and longevity according to
GOD’s design.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life.
I am thankful I wake up each morning yearning to eat healthy foods and
exercise in ways that ensure my HDL, LDL, Triglycerides, and Blood Sugar
levels are ideal.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and correcting, curing, and developing all
healthy aspects of my mind and my body.
I am pleased my blood, heart, arteries, and entire circulatory system
perpetually improves each day.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful I only crave healthy foods that provide my body with the
nutrition necessary for my optimum health and wellbeing.
@@ -0,0 +1,61 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires my body enter deep sleep
and heal miraculously during the night so that I enjoy optimum health.
I am thankful for my health, vitality, and ability to experience deep
healing sleep each and every night.
I appreciate GOD providing strength and direction to every cell in my
body allowing me to sleep deeply and heal all mental, physical, and
spiritual aspects of my being ensuring my optimum overall health.
I am pleased my muscles, bones, joints, organs, nerves, tendons, and
ligaments all heal well during my deep sleep and remain in optimum
condition according to GOD’s design.
I enjoy knowing GOD protects me throughout and beyond the physical
healing and development process.
I am happy my body, mind, and spirit grows stronger each and every day
as a result of my experiencing deep and restful sleep.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life and allowing me to experience deep restful, healing
sleep each night.
I am thankful I am able to easily clear my thoughts when I lay down at
night to sleep, enter deep and restful sleep, and enjoy wonderful
uplifting dreams as my body, mind, and spirit heal, advance, and grow
stronger.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and correcting, curing, and developing all
healthy aspects of my mind and my body during my deep and restful sleep
each night.
I am pleased to wake each morning well rested, energized, and prepared
to face the challenges each day brings with confidence, a positive
attitude, and perseverance.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful I only crave healthy foods that provide my body with the
nutrition necessary to rest well, sleep deep, heal swiftly, and optimize
my health and wellbeing.
@@ -0,0 +1,56 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to believe in him,
trust in him, and follow biblical teachings.
I am thankful for my health, vitality, and motivation to include GOD in
all aspects of my life.
I appreciate GOD providing strength and direction to every cell in my
body which empowers my relationship with him more and more each and
every day.
I am pleased my personal relationship with GOD continues to grow stronger
as I strive to make him a greater influence in my life and live my life
according to GOD’s design.
I enjoy knowing GOD protects me and allows me to develop a deeper and
stronger relationship with him and demonstrate my beliefs and
relationship in all aspects of my life.
I am happy my deepening connection with GOD brings peace and harmony to
my life.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life that I set an outstanding example of living the life of
a true believer.
I am thankful I wake up each morning yearning to share my faith and
understanding of our great Creator.
I appreciate GOD’s almighty power flowing through me and creating
miracles in all areas of my life.
I am pleased my undying faith in GOD allows me to live a relaxed and
perpetually blissful lifestyle.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful I know GOD and share my beliefs with others at every
opportunity.
@@ -0,0 +1,56 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to set and accomplish
clear and concise goals.
I am thankful for my health, vitality, and motivation to accomplish the
goals I set efficiently and effectively.
I appreciate GOD providing strength and direction to every cell in my
body ensuring I plan, focus, and accomplish my goals and live according
to GOD’s design.
I am pleased I can visualize, set, and accomplish goals I once thought
were beyond my abilities so long as they remain according to GOD’s
design.
I enjoy knowing GOD protects me and enables me to accomplish tasks I once
believed were unattainable.
I am happy I think positively and strive to legally and ethically
maximize my accomplishments and my profits.
I am grateful GOD’s healing hands are on me and create miracles in all
areas of my life.
I am thankful I wake up each morning yearning to work and accomplish my
list of goals and tasks in a timely manner.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and enabling me to accomplish my goals
efficiently and effectively.
I am pleased my ability to accomplish tasks and goals efficiently allows
me to live a relaxed and perpetually blissful life.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful that I only crave healthy foods that provide my mind and
body with the nutrition necessary for me to set and achieve all my goals
in record time.
@@ -0,0 +1,53 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to achieve optimum
health.
I am thankful for my health, vitality, and motivation to eat properly,
exercise, and strengthen my body.
I appreciate GOD providing strength and direction to every cell in my
body ensuring optimum health and fitness.
I am pleased my muscles, bones, joints, organs, nerves, tendons, and
ligaments all develop correctly and function normally according to GOD’s
design.
I enjoy knowing GOD protects me throughout and beyond the physical
healing and development process.
I am happy my muscles and bones grow stronger each and every day.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life.
I am thankful that I wake up each morning yearning to eat health foods
and exercise.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and correcting, curing, and developing all
healthy aspects of my mind and my body.
I am pleased my muscles and stamina develop rapidly and I continue
growing perpetually stronger each day.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful I only crave healthy foods that provide my body with the
nutrition necessary for my optimum health and wellbeing.
@@ -0,0 +1,56 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to achieve and
maintain optimum health & wellbeing.
I am thankful for my health, vitality, and motivation to eat properly,
exercise, and maintain a strong and healthy body.
I appreciate GOD providing strength and direction to every cell in my
body ensuring optimum health and wellbeing.
I am pleased for having exceptional mental, physical, and emotional
health and stability allowing me to function at high levels of
efficiency and according to GOD’s design.
I enjoy knowing GOD protects me and keeps me safe from injury and
illness.
I am happy every cell in my body remains strong and my immune system
grows stronger each and every day.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life.
I am thankful that I wake up each morning yearning to eat health foods
and exercise so that I maintain optimum health, wellbeing, and
longevity.
.
I appreciate GOD’s almighty flowing though me, creating miracles in all
areas of my life, and correcting, curing, and developing all healthy
aspects of my mind and my body.
I am pleased my muscles are strong, my body is healthy, I have great
balance, mental clarity, exceptional stamina and I continue growing
perpetually stronger and healthier each day.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful I only crave healthy foods that provide my body with the
nutrition necessary for my optimum health and wellbeing.
@@ -0,0 +1,58 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to be a better, more
compassionate, and understanding partner and spouse each and every day.
I am thankful for my health, vitality, and motivation to be a more
loving, compassionate, understanding, and supportive partner to my spouse
with each passing day.
I appreciate GOD providing strength and direction to every cell in my
body ensuring I continue to grow and develop into a more loyal,
supportive, loving, compassionate, and understanding partner to my
spouse.
I am pleased that I listen attentively to my spouse, ask them pertinent
questions, and allow my spouse to express their feelings, opinion, and
emotions without interruption or judgement.
I enjoy knowing GOD protects me and enables me to be the best and most
loving and supportive spouse I can be.
I am happy that I enjoy a wonderful, fun-filled, loving, and passionate
relationship with my spouse.
I am grateful GOD’s healing hands are on me and create miracles in all
areas of my life.
I am thankful I wake up each morning well rested, excited, and yearning
to spend quality time with my spouse.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and enabling me to fulfill my spouse’s desires
and expectations.
I am pleased spending quality time with my spouse, sharing special
moments, and living a relaxed and perpetually blissful life together.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful that I only crave healthy foods that provide my mind and
body with the nutrition necessary for me to enjoy optimum health and
fulfill all my marital duties & responsibilities.
@@ -0,0 +1,59 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to prioritize and
accomplish my most important tasks throughout each day.
I am thankful for my health, vitality, and motivation to organize and
execute as many tasks as possible each and every day.
I appreciate GOD providing strength and direction to every cell in my
body ensuring I have the strength, focus, and determination to
accomplish my tasks while not wasting my time and energy on
unproductive things like snacking, watching TV, talking without purpose
in person or on the phone, or sleeping too much.
I am pleased for the opportunity to focus on prioritizing and
efficiently accomplishing tasks in order to complete my projects
efficiently and effectively and live my life according to GOD’s design.
I enjoy knowing GOD protects me and provides me with the time, talent,
insight, and focus to accomplish my tasks quickly, accurately, and
completely.
I am happy I am productive each day and my accomplishments are
efficient, effective, and profitable.
I am grateful GOD’s healing hands are on me and create miracles in all
areas of my life.
I am thankful I wake up each morning yearning to eat healthy foods and
exercise in ways that enhance my ability to complete my tasks and
fulfill my responsibilities.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and giving me miraculous insight, as well as
the motivation to be productive every minute of every day.
I am pleased my schedule allows me time to focus on accomplishing my
tasks.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful I am creative, imaginative, constructive, and find great
satisfaction getting things accomplished.
@@ -0,0 +1,60 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to find solace in all
aspects in my life.
I am thankful for my mental wellbeing which allows me to focus on all the
positive aspects of my life, rather than dwelling on depressing,
stressful, or emotionally draining issues that detract from my emotional
stability.
I appreciate GOD providing strength and direction to every cell in my
body, ensuring I remain emotionally stable and maintain a positive
outlook on my life regardless of my situation.
I am pleased I always manage my thoughts, think positive, avoid negative
or depressing thoughts, and live each day with a upbeat, positive, and
emotionally stable state of mind according to GOD’s design.
I enjoy knowing GOD protects me and gives me the mental fortitude to
control my thoughts and emotions while remaining positive and
emotionally stable knowing everything in my life will turn out well.
I am happy I control my own thoughts at all times and no longer dwell on
negative thoughts or issues.
I am grateful GOD’s healing hands are on me and create miracles in all
areas of my life.
I am thankful I wake up each morning with a positive outlook on life that
lasts throughout my entire day and night ensuring I live a peaceful and
emotionally stable life.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and correcting, curing, developing, and
improving my mind, my body, and my emotional stability.
I am pleased my mental health and emotional stability allow me to live a
positive, relaxed, and perpetually blissful lifestyle.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful that I only focus on positive thoughts and crave healthy
foods that provide my mind and body with the nutrition necessary for my
optimum health, emotional wellbeing, and longevity.
@@ -0,0 +1,58 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires my mind and body to heal
miraculously so I no longer suffer from Attention Deficient or Hyper
Activity disorder.
I am thankful for my health, vitality, and ability to control my
hyperactivity, remain calm, pay attention to details, and focus on tasks
through to completion.
I appreciate GOD providing strength and direction to every cell in my
body ensuring efficient, effective, and proper healing of my Attention
Deficient Hyper Activity disorder throughout the entire curative
process.
I am pleased my confidence grows with each passing day, I function
normally, I no longer suffer from low self-esteem, and I no longer have
difficulty maintaining meaningful relationships.
I enjoy knowing GOD protects me throughout and beyond the healing and
developmental process as I no longer suffer from Attention Deficient
Hyper Activity disorder.
I am happy my ability to focus grows stronger each and every day.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life.
I am thankful I remain calm and in control of my hyperactivity.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and correcting, curing, and developing all
healthy aspects of my mind and my body.
I am pleased I am fully capable of being attentive and consistently
focusing on any and all projects and situations I encounter.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful I do not suffer from Attention Deficient Hyper Activity
disorder and only crave healthy foods that provide my body with the
nutrition necessary to optimize my health and wellbeing.
@@ -0,0 +1,58 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to achieve deep
relaxation, peace, and harmony in all aspects of my life.
I am thankful for my health, vitality, and motivation to eat properly,
exercise, and always maintain control of my thoughts, emotions, and
mental state.
I appreciate GOD providing strength and direction to every cell in my
body ensuring I am emotionally stable and maintain a relaxed and focused
attitude regardless of my personal situation.
I am pleased I always think positive, manage my thoughts, and live each
day in a relaxed and emotionally stable state of mind according to GOD’s
design.
I enjoy knowing GOD protects me and allows me to remain relaxed knowing
everything in my life will turn out well.
I am happy I think positively, I do not dwell on negative issues, and I
control my thoughts at all times.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life.
I am thankful I wake up each morning yearning to eat healthy foods and
exercise in ways that ensure I live a peaceful and emotionally stable
life.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and correcting, curing, and developing all
healthy aspects of my mind and my body for the better.
I am pleased my mental health and emotional stability allow me to live a
relaxed and perpetually blissful life.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful that I only crave healthy foods that provide my mind and
body with the nutrition necessary for my optimum health, wellbeing, and
longevity.
@@ -0,0 +1,59 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me causing me to release stress, release
anxiety, and live a calm and peaceful life.
I am thankful for my health, vitality, and the motivation to breathe
deeply, eat slowly, consume nutritious foods, and exercise as a means of
releasing my stress, worries, anxiety, and frustration.
I appreciate GOD providing strength and direction to every cell in my
body ensuring I live free from stress, worry, uncertainty, and
nervousness.
I am pleased I live a stress free, happy, and fun filled life with
exceptional mental, physical, and emotional health and stability.
I enjoy knowing I can rely upon my relationship with GOD knowing GOD
protects me and keeps me safe so I have no reason to stress, no reason
to worry, no reason for uncertainty, and no reason to be nervous.
I am happy every cell in my body remains strong and the confidence in my
ability to control my life and my future grows stronger each and every
day.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life.
I am thankful that I wake up each morning yearning to confidently take on
whatever tasks come my way without stress, worry, uncertainly, or
nervousness.
.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and correcting, curing, and developing all
healthy aspects of my mind and my body.
I am pleased with my mental clarity, my ability to remain calm and in
control off my activities, and focus on tasks without distraction as I
continue growing perpetually stronger and healthier each day.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful that I control my own destiny without fear, without
stress, without worry, without uncertainty, and without nervousness.
@@ -0,0 +1,52 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires my body to heal
miraculously, quickly achieving optimum health.
I am thankful for my health, vitality, and ability to heal quickly.
I appreciate GOD providing strength and direction to every cell in my
body ensuring efficient, effective, and proper healing throughout the
curative process.
I am pleased my muscles, bones, joints, organs, nerves, tendons, and
ligaments all heal correctly and function normally according to GOD’s
design.
I enjoy knowing GOD protects me throughout and beyond the physical
healing and development process.
I am happy my muscles and bones grow stronger each and every day.
I am grateful GOD’s healing hands are on me creating miracles in all
areas of my life.
I am thankful my pain remains tolerable throughout the healing process.
I appreciate GOD’s almighty power flowing though me, creating miracles
in all areas of my life, and correcting, curing, and developing all
healthy aspects of my mind and my body.
I am pleased my injuries heal rapidly and I emerge stronger and healthier
than before my injury.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful I only crave healthy foods that provide my body with the
nutrition necessary to heal swiftly and optimize my health and wellbeing.
@@ -0,0 +1,56 @@
shall the mind and body now: Project its awareness, attention, belief, and
intention into all times dimensions of undivided universal field of
consciousness and identity, accept and release attunement to all events,
information faults, and information faults hidden that compromises
awareness of, or define into existence, the transformed-from state? Or,
that cause this transformative information quanta to define into existence
the transformed-to system state? Or that cause this transformation to
seem dangerous, undeserved, or impossible to achieve? Create awareness,
acceptance, attunement, and belief of all corrective events and
transformative information quanta to define into existence the
transformed-to system state?
Create the intention to collapse the quantum potential the field’s reality
quantum potential of the field’s reality quant to manifest this
transformation? Create optimal attunement and coherence to the source of
creation, GOD, innate intelligence, and the consciousness of healing and
accept the role of co-creator of all experienced reality, NOW?
I am grateful GOD lives through me and inspires me to eat right,
exercise, and reach my ideal weight quickly and effectively.
I am thankful for my health, vitality, and motivation to eat properly,
exercise, and strengthen my body while losing unhealthy and unwanted fat.
I am appreciate GOD providing strength and direction to every cell in my
body ensuring I control my food intake, exercise, and reach my weight
loss goal.
I am pleased for having the time to buy and prepare healthy foods,
exercise, and focus on my overall health.
I enjoy knowing GOD protects me and inspires me to become healthy and fit
and live my life according to GOD’s design.
I am happy I have the desire and power to avoid eating sugary foods and
unhealthy snacks in order control my caloric intake and lose excess
weight.
I am grateful GOD’s healing hands are on me and create miracles in all
areas of my life.
I am thankful I wake up each morning yearning to eat healthy foods and
exercise in ways that ensure I reach and maintain my ideal body weight.
I appreciate GOD’s almighty power flowing though me and empowering me to
do whatever it takes to reach my ultimate weight and fitness level.
I am pleased my continued efforts to reach and maintain my ultimate
weight and fitness are rewarded with increased energy, great stamina, and
good mental and physical health.
I am grateful to GOD for all the blessings bestowed upon me.
I am thankful that I only crave healthy foods that provide my mind and
body with the nutrition necessary for my optimum health, wellbeing, and
longevity.
+13
View File
@@ -0,0 +1,13 @@
#version 330 core
in vec2 pass_TexCoord;
out vec4 out_Color;
uniform sampler2D u_fontTexture;
uniform vec4 u_textColor;
void main()
{
vec4 texel = texture(u_fontTexture, pass_TexCoord);
float mask = texel.a;
out_Color = vec4(u_textColor.rgb, u_textColor.a * mask);
}
+12
View File
@@ -0,0 +1,12 @@
#version 330 core
layout (location = 0) in vec2 in_Position;
layout (location = 1) in vec2 in_TexCoord;
out vec2 pass_TexCoord;
uniform mat4 u_mvpMatrix;
void main()
{
gl_Position = u_mvpMatrix * vec4(in_Position.x, in_Position.y, 0.0, 1.0);
pass_TexCoord = in_TexCoord;
}
+9
View File
@@ -0,0 +1,9 @@
#version 330 core
out vec4 FragColor;
uniform vec4 u_color;
void main() {
FragColor = u_color;
}
+9
View File
@@ -0,0 +1,9 @@
#version 330 core
layout (location = 0) in vec2 aPos;
uniform mat4 u_mvpMatrix;
void main() {
gl_Position = u_mvpMatrix * vec4(aPos.xy, 0.0, 1.0);
}
+17
View File
@@ -0,0 +1,17 @@
#version 330 core
// The final color output for the pixel
out vec4 FragColor;
// Input from the vertex shader
in vec2 TexCoord; // The texture coordinate, interpolated for this specific pixel
// The source texture (our FontMap "sticker sheet")
uniform sampler2D u_texture;
uniform vec4 u_textColor;
void main()
{
float mask = texture(u_texture, TexCoord).a;
FragColor = vec4(u_textColor.rgb, u_textColor.a * mask);
}
+21
View File
@@ -0,0 +1,21 @@
#version 330 core
// Input vertex data from the VBO
layout (location = 0) in vec2 aPos; // The (x, y) position of the vertex
layout (location = 1) in vec2 aTexCoord; // The (u, v) texture coordinate for this vertex
// Output to the fragment shader
out vec2 TexCoord;
// A matrix to handle projection (not strictly needed for this specific render-to-texture,
// but good practice to include for general purpose use)
uniform mat4 u_mvpMatrix;
void main()
{
// Set the final position of the vertex
gl_Position = u_mvpMatrix * vec4(aPos.x, aPos.y, 0.0, 1.0);
// Pass the texture coordinate straight through to the fragment shader
TexCoord = aTexCoord;
}
+15
View File
@@ -0,0 +1,15 @@
<p>Welcome to RHEMA WAVE TECHNOLOGY!</p>
<p>Please review the following screens carefully and completely as they provide important user information, as well as outline YOUR legal responsibilities!</p>
<p>First and foremost, congratulations! Congratulations to YOU for having the foresight to invest in what we believe is one of the greatest breakthroughs in the history of humanity! More importantly, we congratulate YOU for investing in YOU!</p>
<p>We would also like to express our appreciation for YOUR faith, patience, and understanding as we continue evolving our ingenious business model, our revolutionary patented products, and our unprecedented copy-written technology! While we have, quoting the immortal words of Captain Kirk in the original Star Trek series, "Gone where no man has gone before!" we have already complied quite a long list of futuristic products, services, refinements, and innovations we expect to introduce as soon as humanly possible.</p>
<p>While RHEMA WAVE TECHNOLOGY is already on the cutting edge of technological advancement, we believe this is just the beginning as we intend to push the cutting edge of technology for decades into the future with yet unimaginable creations. Our focus on the continuing evolution of our science will ensure competitors will fall by the wayside as RHEMA WAVE TECHNOLOGY continues to stand alone as the undisputed "Pioneer of Innovation!"</p>
<p>Welcoming you into our RHEMA WAVE TECHNOLOGY family is far more than simply a polite slogan. We consider our members kin to the RHEMA WAVE TECHNOLOGY lineage and thus part of a very unique and special family. As part of our family we anticipate YOU will remain in communication with us and share YOUR personal progress and experiences. We also encourage you to ask questions, express concerns, and share your ideas as everyone's continued involvement is key to continued advancement.</p>
<p>In closing, and keeping with the original Star Trek theme, we feel it is amazingly appropriate to salute you in the same manner Spock, the half-human, half-Vulcan, pointy eared, logic-driven first officer aboard the StarShip Enterprise would,</p>
<p>"Live long &amp; Prosper!"</p>
+15
View File
@@ -0,0 +1,15 @@
<p>RHEMA WAVE TECHNOLOGY, EQUIPMENT, AND SOFTWARE IS NOT INTENDED, MARKETED, OR SOLD AS A MEDICAL DEVICE.</p>
<p>RHEMA WAVE TECHNOLOGY, EQUIPMENT, AND SOFTWARE IS NOT INTENDED TO DIAGNOSE, TREAT, OR CURE ANY ILLNESS, AILMENT, OR DISEASE.</p>
<p>RHEMA WAVE TECHNOLOGY, EQUIPMENT, AND SOFTWARE MERELY CREATES WHAT WE BELIEVE TO BE A SUBTLE ENERGY FIELD THAT LIVING CELLS SEEM TO ABSORB. THIS ABSORBED ENERGY APPEARS TO ALLOW CELLS TO FUNCTION MORE EFFICIENTLY &amp; POTENTIALLY MORE EFFECTIVELY THAN THEY NORMALLY WOULD UNDER MODERN DAY STRESS AND THE ENVIRONMENT IN WHICH WE CURRENTLY LIVE.</p>
<p>RHEMA WAVE TECHNOLOGY MAKES NO CLAIMS AS TO THE EFFECTS, EFFECTIVENESS, OR RESULTS FROM USING RHEMA WAVE TECHNOLOGY PRODUCTS AND HEREBY DISCLAIMS ANY REPRESENTATION OR WARRANTY TO THAT EFFECT.</p>
<p>USER/BUYER UNDERSTANDS AND AGREES NO MEDICAL OR HEALING CLAIMS ARE TO BE MADE REGARDING RHEMA WAVE TECHNOLOGY AND RHEMA WAVE SYSTEMS, TO INCLUDE SOFTWARE, HARDWARE, AND PROGRAMMING.</p>
<p>USER/BUYER UNDERSTANDS AND AGREES IT IS USER/BUYER'S SOLE RESPONSIBILITY TO INFORM ANY AND ALL USERS THEY INVITE OR ALLOW TO EXPERIENCE THEIR RHEMA WAVE SYSTEM OF THESE TERMS AND CONDITIONS.</p>
<p>USER/BUYER UNDERSTANDS AND AGREES USER/BUYER SHALL BE SOLELY RESPONSIBLE FOR COMPENSATION TO RHEMA WAVE TECHNOLOGY FOR ANY AND ALL CLAIMS, FINES, DAMAGES, AND LEGAL FEES RESULTING FROM USER/BUYER MAKING SUCH UNAUTHORIZED COMMENTS, INCLUDING THOSE LEVIED AGAINST RHEMA WAVE TECHNOLOGIES BY PERSONS, CORPORATIONS, AND FEDERAL AGENCIES.</p>
<p>IT IS ALSO UNDERSTOOD EQUIPMENT PROVIDERS SHALL BE HELD HARMLESS AND NOT BE RESPONSIBLE FOR ANY AND ALL SUCH CLAIMS.</p>
@@ -0,0 +1,15 @@
<p>INSTRUCTIONS FOR USE</p>
<p>1. Ensure you have adequate internet access.<br>
2. Enter a valid email address for your account.<br>
3. Enter a license key for your account. In the near future each account will be assigned a License Key but, at this point in time, enter your first and last name followed 1 2 3 4 etc. with no spaces until you achieve a total of 16 characters. Your License Key identifies your RHEMA SCALAR WAVE GENERATOR and its enabled features, as well as grants access to your account for activation, operation, features, and updates.<br>
4. Begin by using the F3 key to access your systems features and you will see:</p>
<p>PROCLAMATIONS<br>
SCHEDULER<br>
WiFi SETUP<br>
AUDIO OPTIONS<br>
EMAIL ADDRESS<br>
LICENSE KEY</p>
<p>Using the keyboard arrow keys you can scroll through the options and chose the one you would like to access by using the return key. The PROCLAMATIONS key will take you to a host of proclamations and options. The SCHEDULER key grants access a scheduling system allowing your to create varies different proclamation sessions, which I will explain shortly. The WiFi SETUP allows you to change WiFi networks. The AUDIO OPTIONS allows you to switch audio on or off, change audio devices, and switch between audio genres. EMAIL ADDRESS allows you to change your Email Address. License Key allows you to access and change your License Key provided RHEMA WAVE TECHNOLOGIES is notified in advance of the change.</p>
@@ -0,0 +1,13 @@
<p>PROCLAMATIONS</p>
<p>Use keyboard arrow keys and return key to select Proclamations. There you will find several options for activating monthly access to proclamations and RHEMA WAVE SAAS (software as a service).</p>
<p>Option A: includes monthly access to SAAS, 10 proclamations of your choice, plus $1 for each additional activated proclamation.</p>
<p>Option B: includes monthly access to SAAS and 25 proclamations of your choice, plus 75 cents for each additional activated proclamation.</p>
<p>Option C: includes monthly access to SAAS and 50 proclamations of your choice, plus 50 cents for each additional activated proclamation.</p>
<p>Option D: includes monthly access to SAAS and access to all available proclamations.</p>
<p>Begin by using arrow keys and return key to select an proclamation category, then use the up and down arrow keys to scroll through all the proclamations in each category so that you are familiar with the complete library. You may want to consider making a list of the proclamations you would like to start with as this will simplify the process of selecting, activating, and scheduling your chosen proclamations.</p>
@@ -0,0 +1,5 @@
<p>Ideally we suggest using the SCHEDULER to create at least 4 separate lists of proclamations, one to start your morning, perhaps starting 30 minutes before your scheduled waking up time, one for an afternoon sessions, one for an evening "wind down" session, and a nighttime sleeping session. The SCHEDULER allows you to determine when one series of proclamations ends and the next series begins. Depending upon your particular schedule you may want to add more sessions. Upon completion of your review select desired proclamations by simply pressing the space bar. Activated proclamations appear GREEN, while inactive proclamations remain WHITE.</p>
<p>We highly suggest (and would require if we could) activating Shangri-La in ALL sessions ALL the time so it runs constantly from now to eternity (or at least until we have the time to create a few alternatives)! Shangri-La is a relaxing, full body cellular supercharger designed to boost energy throughout the entire body. In addition to Shangri-La, we highly suggest activating 3 to 5 priority proclamations that also run in all sessions (continuously 24 hours a day) until you are satisfied with your results. Once you are satisfied with your results you can simply return to the SCHEDULER and select a different proclamation to take its place. A priority proclamation is an proclamation focused on a particular area or issue and easily recognized by an accompanying asterisk (*).</p>
<p>General proclamations are those focused on desired behavioral changes and NOT marked with an asterisk (*). We suggest activating 10 to 20 different general proclamations for each scheduled session. Should you have sincere interest in certain general proclamations such as Health &amp; Well-Being, Weight-Loss, or Reduce Stress, Anxiety, and Panic Attacks they can be included in more than one session, depending upon the severity and your desire to seek improvement. While initially this may seem like quite a few proclamations, other than Shangri-La, which runs a bit longer, most other proclamations are communicated in under 30 seconds so programming 20 proclamations, each would repeat 6 times an hour.</p>
@@ -0,0 +1,9 @@
<p>Now for the fun part! To ensure complete understanding I will explain a few more details about proclamations and make a few more recommendations.</p>
<p>While proclamations like Deep Sleep, Relaxation, and Bathe in Blessings connote peace, rest, and relaxation, proclamations like Goal Setting &amp; Achievement, Achieving Success, Mastering Money Management, and Maximize My Productivity exude connotations of action and mental activity. Accordingly, we recommend scheduling relaxing proclamations like Relaxation, Bathe in Blessings, and Deep Sleep for evening wind down sessions and especially nighttime sleep sessions, with more aggressive proclamations ideally activated first thing in the morning and though out the day.</p>
<p>We highly suggest including proclamations like Deep Sleep, Vivid Dreams, and Heavenly Communication in your nighttime sleep session from the very beginning. Activating Deep Sleep in particular in nighttime sleep sessions is the simplest, fastest, and most efficient way to experience the effectiveness of RHEMA WAVES. Those who follow this recommendation report waking up amazed at how well-rested they feel and deep they slept. Just one undeniable personal experience such as this will do more to convince you just how amazing this technology truly is, as well as deepen your appreciation for the investment you are making in you than words ever could.</p>
<p>Now, those of you lucky enough to work from home will certainly have an advantage spending more time in RHEMA WAVES and all your selected sessions. Even though your system may be set up in your bedroom and you work in your office or dining room, the RHEMA WAVES will still have an impact outside the main location, it just won't be as effective.</p>
<p>Those of you not lucky enough to work from home should still use the SCHEDULER to create sessions. Having a morning session running while preparing for work as well as an evening session will be beneficial, as well as enjoying full sessions on weekends and whenever you spend more time at home. If you work outside the home and wish to make the most of your time in RHEMA WAVES, you can add more aggressive proclamations to your sleep sessions, but be sure to include Deep Sleep and a couple other relaxing proclamations along with them. You can also alter your general nighttime sleep session proclamations every couple of weeks, or as soon as you are satisfied with the session's effects.</p>
@@ -0,0 +1,5 @@
<p>Obviously the variety of proclamations accessible to you will be determined by your choice of proclamation subscription. Please know that, while each specific proclamation takes anywhere from several hours to several days to create, we remain diligent and continue conducting research and developing additional proclamations, so you can anticipate continued expansion of our proclamation catalog.</p>
<p>Each specific proclamation, as referenced by their titles, is meticulously designed and painstakingly created incorporating detailed information and very specific facts thoughtfully programmed to travel along our patented RHEMA WAVE ENERGY FIELD. The average human body is made up of approximately 37 trillion cells (plus or minus depending on your size and weight). Each of these cells appear capable of absorbing energy from RHEMA WAVE ENERGY FIELD and function more efficiently and effectively as a result. Results suggest we are capable of focusing a portion of the absorbed RHEMA WAVE energy on the specific area, habit, trait, or issue described in each proclamation's title.</p>
<p>Either that, or we have simply created the most consistently effective placebo generator in the history of placebo generators! All I know is, if I, or someone I care about, was suffering from a dreadful condition and that dreadful condition was alleviated, I would be exceedingly happy the dreadful condition was gone, and far less concerned with how, why, or where it went! While all this may sound farfetched, the results show otherwise!</p>
@@ -0,0 +1,11 @@
<p>Okay, all humor aside for this paragraph! Under U.S. federal law, specifically the Federal Food, Drug, and Cosmetic Act, a drug is generally defined as any substance intended for use in the diagnosis, cure, mitigation, treatment, or prevention of disease in humans or animals, or any substance other than food intended to affect the structure or function of the body. Thus, it is imperative anyone and everyone involved with RHEMA WAVE TECHNOLOGY understands that RHEMA WAVE TECHNOLOGY does not promote or provide drugs, medications, supplements, massage, manipulation, salves, ointments, injections, adjustments, surgeries, radiation, or any other therapies for that matter! Additionally, RHEMA WAVE TECHNOLOGY does not diagnose, cure, mitigate, treat, or prevent diseases in humans or animals!</p>
<p>RHEMA WAVE TECHNOLOGY merely provides an environment offering your 37 trillion cells (+/-) the opportunity to recharge and hopefully function more efficiently and effectively. With this understanding it may be helpful to imagine the words "Focus On" in front of each proclamation title, as we believe focusing energy to certain predetermined areas simply allows your body to do what only it knows how to do!</p>
<p>In order to maximize the effectiveness of your system we recommend placing it in your bedroom. This recommendation is based upon three factors:<br>
1) The conscious mind is dormant during sleep, while the subconscious mind is not; thereby alleviating any conscious resistance.<br>
2) Nighttime sleep sessions offer the largest amount of consecutive, uninterrupted, and disturbance free time our bodies have.<br>
3) Our bodies generally heal more during sleep because we are inactive and not using energy for other purposes.<br>
4) Results: those who sleep in the system tend to notice better and faster results, which is likely due to 1, 2, &amp; 3!</p>
<p>Understandably, there are certainly reasons for placing your system in other rooms, such as a living room in order to share the experience with family members, in an office if that is where you work, or wherever you generally spend the majority of your time. Regardless of where you place your system it will energize your home beyond the walls of the room where it resides; but obviously to a lesser extent. While the RHEMA WAVES will not be as effective outside the main area, customers who set their system up in their family room have called the very next day sharing how amazed they were at how well they slept even though the system was running in another room!</p>
+7
View File
@@ -0,0 +1,7 @@
<p>Okay, I have a confession to make and this seems like a good place to make it. I was lucky enough to find a very successful mentor who, in a matter months, taught me more about relationships, business, success, happiness, and just plain life than I ever learned in 16 years of school and college! I say that because, while the lessons I learned from him allowed me to reach the top 1% money earners on the planet and live a lifestyle few will ever experience, none of what I learned from my mentor is taught in school or college! Both what he taught me and the results I achieved following his advice not only inspired me to pursue this unprecedented opportunity, it also provided me with the wherewithal to conduct the research, develop and test designs, conduct experiments, and ultimately create RHEMA WAVE TECHNOLOGY! The miraculous products we currently offer, advances and refinements to those products, as well as products currently under development and yet to be conceived are all the result of what I learned under his mentorship. So, in some small way, my mentor is having an impact on you too!</p>
<p>When I was first introduced to him he quickly realized my demeanor was very serious, critical, and structured. I had assumed this was how people conducted themselves in order to become successful. I was hoping he would be impressed with my performance because, in reality, I was broke, confused about how the world works, and desperately in need of guidance. The very first thing he said to me was, "If we are ever going to work together you must guarantee me one thing!" At that point in my life I was willing to do just about anything to learn what I knew I did not know and said, "Okay!" He said, "You must stop taking life so serious! And you must learn to NEVER, EVER lose your sense of humor! One thing you must understand is none of us are getting out of here alive anyway, so we might as well enjoy ourselves while we are here! Don't ever forget it!" I haven't.</p>
<p>So if you are the kind of person whose demeanor is serious, critical, and structured, and you are unwilling to change, can't find a sense of humor, and refuse to stop taking life so serious, please return your system before you venture any further! Above all, the most important thing my mentor taught me was life is much better, more fun, and more fulfilling when we avoid dealing with people like I used to be! So, if you are committed to being like who I used to be, be a sport, send your system back, let those of us looking to enjoy health, vitality, happiness, and fulfillment enjoy the benefits of RHEMA WAVE TECHNOLOGY and just go on your merry way!</p>
<p>As the Chief Developer who believed wholeheartedly in this amazing technology from the very beginning, I willingly and excitedly became the first RHEMA WAVE test subject. Almost immediately my home became my laboratory where every design, experiment, alteration, and advancement was tried and tested on me and my family. For well over a year we have been basking in RHEMA WAVES 24-hours a day, 7 days a week except for the occasional business meeting, short trips to the grocery store, or attending church! Currently we have 6 complete systems constantly running in our home simultaneously, 3 in bedrooms, 1 in the office, 1 in our kitchen/family room, and 1 in the home theater. Me, my wonderful wife, and our incredible children have been basking in, and continuously surrounded by RHEMA WAVES for well over a year with nothing but positive results to show for it! I purposely surrounded myself and my family with RHEMA WAVES for the sole purpose of proving the effectiveness and documenting NO NEGATIVE ADVERSE SIDE EFFECTS; that is how much I believe in RHEMA WAVE TECHNOLOGY!</p>
@@ -0,0 +1,24 @@
<p>Now, the question I am asked most often, and the question likely on your mind is, "How long will it take before I notice changes?" Well, it depends:</p>
<p>1) How old you are.<br>
2) How many years/months you have been dealing with the issue.<br>
3) What issues you will be striving to influence.<br>
4) How many total issues you will be striving to influence.<br>
5) How many issues you will be striving to influence simultaneously.<br>
6) The severity of the issue(s) you will be striving to influence.<br>
7) Where your system is located.<br>
8) Primary activities taking place where your system is located: sleeping, working, eating, watching TV, exercising, etc.<br>
9) Number of hours a day spent in the room where your system is located.<br>
10) Whether you are genuinely excited about spending time in RHEMA WAVES and looking forward to positive results, or skeptical, pessimistic, and doubting you will experience positive influences.</p>
<p>Each of these factors will have a profound affect on your results. While we are still unsure the extent to which a pessimistic attitude detracts from results, we do know those excited about spending time in RHEMA WAVES seem to achieve positive results sooner. Positive attitudes = Positive results! Negative attitudes = positive results too! (it just takes a bit more time!) If you are pessimistic, we suggest you including several "Attitude" and "Happiness" proclamations from the start!</p>
<p>I actually heard from one of our beta testers early on who set their system up in their office because they spend more time in the office than in the bedroom. I recommended beginning with several proclamations including "Deep Sleep." They called me the very next morning amazed at how deeply they slept, how well rested they felt, and how their mind felt very clear! This despite the fact they rolled up in a blanket and slept on the office floor!</p>
<p>We are excited for YOU and are available to answer your questions, satisfy your concerns, and entertain your ideas. We look forward to hearing from you and learning of your personal experiences as you bask in our RHEMA WAVE ENERGY FIELD! Always know that we will be basking right along with you!</p>
<p>Welcome to a whole new world,</p>
<p>RHEMA WAVE TECHNOLOGY<br>
(817) 864-1111<br>
rhemawavetechnology.com</p>
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Eric Woroshow
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.
+5
View File
@@ -0,0 +1,5 @@
//go:build required
// +build required
// Package dummy prevents go tooling from stripping the c dependencies.
package dummy
+290
View File
@@ -0,0 +1,290 @@
#ifndef __khrplatform_h_
#define __khrplatform_h_
/*
** Copyright (c) 2008-2018 The Khronos Group Inc.
**
** Permission is hereby granted, free of charge, to any person obtaining a
** copy of this software and/or associated documentation files (the
** "Materials"), to deal in the Materials without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Materials, and to
** permit persons to whom the Materials are 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 Materials.
**
** THE MATERIALS ARE 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
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
/* Khronos platform-specific types and definitions.
*
* The master copy of khrplatform.h is maintained in the Khronos EGL
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
* The last semantic modification to khrplatform.h was at commit ID:
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
*
* Adopters may modify this file to suit their platform. Adopters are
* encouraged to submit platform specific modifications to the Khronos
* group so that they can be included in future versions of this file.
* Please submit changes by filing pull requests or issues on
* the EGL Registry repository linked above.
*
*
* See the Implementer's Guidelines for information about where this file
* should be located on your system and for more details of its use:
* http://www.khronos.org/registry/implementers_guide.pdf
*
* This file should be included as
* #include <KHR/khrplatform.h>
* by Khronos client API header files that use its types and defines.
*
* The types in khrplatform.h should only be used to define API-specific types.
*
* Types defined in khrplatform.h:
* khronos_int8_t signed 8 bit
* khronos_uint8_t unsigned 8 bit
* khronos_int16_t signed 16 bit
* khronos_uint16_t unsigned 16 bit
* khronos_int32_t signed 32 bit
* khronos_uint32_t unsigned 32 bit
* khronos_int64_t signed 64 bit
* khronos_uint64_t unsigned 64 bit
* khronos_intptr_t signed same number of bits as a pointer
* khronos_uintptr_t unsigned same number of bits as a pointer
* khronos_ssize_t signed size
* khronos_usize_t unsigned size
* khronos_float_t signed 32 bit floating point
* khronos_time_ns_t unsigned 64 bit time in nanoseconds
* khronos_utime_nanoseconds_t unsigned time interval or absolute time in
* nanoseconds
* khronos_stime_nanoseconds_t signed time interval in nanoseconds
* khronos_boolean_enum_t enumerated boolean type. This should
* only be used as a base type when a client API's boolean type is
* an enum. Client APIs which use an integer or other type for
* booleans cannot use this as the base type for their boolean.
*
* Tokens defined in khrplatform.h:
*
* KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
*
* KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
* KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
*
* Calling convention macros defined in this file:
* KHRONOS_APICALL
* KHRONOS_APIENTRY
* KHRONOS_APIATTRIBUTES
*
* These may be used in function prototypes as:
*
* KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
* int arg1,
* int arg2) KHRONOS_APIATTRIBUTES;
*/
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
# define KHRONOS_STATIC 1
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APICALL
*-------------------------------------------------------------------------
* This precedes the return type of the function in the function prototype.
*/
#if defined(KHRONOS_STATIC)
/* If the preprocessor constant KHRONOS_STATIC is defined, make the
* header compatible with static linking. */
# define KHRONOS_APICALL
#elif defined(_WIN32)
# define KHRONOS_APICALL __declspec(dllimport)
#elif defined (__SYMBIAN32__)
# define KHRONOS_APICALL IMPORT_C
#elif defined(__ANDROID__)
# define KHRONOS_APICALL __attribute__((visibility("default")))
#else
# define KHRONOS_APICALL
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIENTRY
*-------------------------------------------------------------------------
* This follows the return type of the function and precedes the function
* name in the function prototype.
*/
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
/* Win32 but not WinCE */
# define KHRONOS_APIENTRY __stdcall
#else
# define KHRONOS_APIENTRY
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIATTRIBUTES
*-------------------------------------------------------------------------
* This follows the closing parenthesis of the function prototype arguments.
*/
#if defined (__ARMCC_2__)
#define KHRONOS_APIATTRIBUTES __softfp
#else
#define KHRONOS_APIATTRIBUTES
#endif
/*-------------------------------------------------------------------------
* basic type definitions
*-----------------------------------------------------------------------*/
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
/*
* Using <stdint.h>
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(__VMS ) || defined(__sgi)
/*
* Using <inttypes.h>
*/
#include <inttypes.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
/*
* Win32
*/
typedef __int32 khronos_int32_t;
typedef unsigned __int32 khronos_uint32_t;
typedef __int64 khronos_int64_t;
typedef unsigned __int64 khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(__sun__) || defined(__digital__)
/*
* Sun or Digital
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#if defined(__arch64__) || defined(_LP64)
typedef long int khronos_int64_t;
typedef unsigned long int khronos_uint64_t;
#else
typedef long long int khronos_int64_t;
typedef unsigned long long int khronos_uint64_t;
#endif /* __arch64__ */
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif 0
/*
* Hypothetical platform with no float or int64 support
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#define KHRONOS_SUPPORT_INT64 0
#define KHRONOS_SUPPORT_FLOAT 0
#else
/*
* Generic fallback
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#endif
/*
* Types that are (so far) the same on all platforms
*/
typedef signed char khronos_int8_t;
typedef unsigned char khronos_uint8_t;
typedef signed short int khronos_int16_t;
typedef unsigned short int khronos_uint16_t;
/*
* Types that differ between LLP64 and LP64 architectures - in LLP64,
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
* to be the only LLP64 architecture in current use.
*/
#ifdef _WIN64
typedef signed long long int khronos_intptr_t;
typedef unsigned long long int khronos_uintptr_t;
typedef signed long long int khronos_ssize_t;
typedef unsigned long long int khronos_usize_t;
#else
typedef signed long int khronos_intptr_t;
typedef unsigned long int khronos_uintptr_t;
typedef signed long int khronos_ssize_t;
typedef unsigned long int khronos_usize_t;
#endif
#if KHRONOS_SUPPORT_FLOAT
/*
* Float type
*/
typedef float khronos_float_t;
#endif
#if KHRONOS_SUPPORT_INT64
/* Time types
*
* These types can be used to represent a time interval in nanoseconds or
* an absolute Unadjusted System Time. Unadjusted System Time is the number
* of nanoseconds since some arbitrary system event (e.g. since the last
* time the system booted). The Unadjusted System Time is an unsigned
* 64 bit value that wraps back to 0 every 584 years. Time intervals
* may be either signed or unsigned.
*/
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
typedef khronos_int64_t khronos_stime_nanoseconds_t;
#endif
/*
* Dummy value used to pad enum types to 32 bits.
*/
#ifndef KHRONOS_MAX_ENUM
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
#endif
/*
* Enumerated boolean type
*
* Values other than zero should be considered to be true. Therefore
* comparisons should not be made against KHRONOS_TRUE.
*/
typedef enum {
KHRONOS_FALSE = 0,
KHRONOS_TRUE = 1,
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
} khronos_boolean_enum_t;
#endif /* __khrplatform_h_ */
+17
View File
@@ -0,0 +1,17 @@
//go:build required
// +build required
package gl
// This file exists purely to prevent the golang toolchain from stripping
// away the c source directories and files when `go mod vendor` is used
// to populate a `vendor/` directory of a project depending on `go-gl/go`.
//
// How it works:
// - every directory which only includes c source files receives a dummy.go file.
// - every directory we want to preserve is included here as a _ import.
// - this file is given a build tag to exclude it from the regular build.
import (
// Prevent go tooling from stripping out the c source files.
_ "github.com/go-gl/gl/v3.3-core/gl/KHR"
)
+116
View File
@@ -0,0 +1,116 @@
// Code generated by glow (https://github.com/go-gl/glow). DO NOT EDIT.
package gl
import (
"fmt"
"reflect"
"strings"
"unsafe"
)
// #include <stdlib.h>
import "C"
// Ptr takes a slice or pointer (to a singular scalar value or the first
// element of an array or slice) and returns its GL-compatible address.
//
// For example:
//
// var data []uint8
// ...
// gl.TexImage2D(gl.TEXTURE_2D, ..., gl.UNSIGNED_BYTE, gl.Ptr(&data[0]))
func Ptr(data interface{}) unsafe.Pointer {
if data == nil {
return unsafe.Pointer(nil)
}
var addr unsafe.Pointer
v := reflect.ValueOf(data)
switch v.Type().Kind() {
case reflect.Ptr:
e := v.Elem()
switch e.Kind() {
case
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
addr = unsafe.Pointer(e.UnsafeAddr())
default:
panic(fmt.Errorf("unsupported pointer to type %s; must be a slice or pointer to a singular scalar value or the first element of an array or slice", e.Kind()))
}
case reflect.Uintptr:
addr = unsafe.Pointer(data.(uintptr))
case reflect.Slice:
addr = unsafe.Pointer(v.Index(0).UnsafeAddr())
default:
panic(fmt.Errorf("unsupported type %s; must be a slice or pointer to a singular scalar value or the first element of an array or slice", v.Type()))
}
return addr
}
// PtrOffset takes a pointer offset and returns a GL-compatible pointer.
// Originally intended for functions such as glVertexAttribPointer that take pointer
// parameters also for offsets, since Go 1.14 this is no longer recommended.
//
// Use a corresponding offset-compatible variant of the function instead.
// For example, for gl.VertexAttribPointer() there is gl.VertexAttribPointerWithOffset().
//
// See https://github.com/go-gl/gl#go-114-and-checkptr for more details on the checkptr detector.
// See https://github.com/go-gl/glow#overloads, about adding new overloads.
//
// Deprecated: Use more appropriate overload function instead
func PtrOffset(offset int) unsafe.Pointer {
return unsafe.Pointer(uintptr(offset))
}
// Str takes a null-terminated Go string and returns its GL-compatible address.
// This function reaches into Go string storage in an unsafe way so the caller
// must ensure the string is not garbage collected.
func Str(str string) *uint8 {
if !strings.HasSuffix(str, "\x00") {
panic("str argument missing null terminator: " + str)
}
header := (*reflect.StringHeader)(unsafe.Pointer(&str))
return (*uint8)(unsafe.Pointer(header.Data))
}
// GoStr takes a null-terminated string returned by OpenGL and constructs a
// corresponding Go string.
func GoStr(cstr *uint8) string {
return C.GoString((*C.char)(unsafe.Pointer(cstr)))
}
// Strs takes a list of Go strings (with or without null-termination) and
// returns their C counterpart.
//
// The returned free function must be called once you are done using the strings
// in order to free the memory.
//
// If no strings are provided as a parameter this function will panic.
func Strs(strs ...string) (cstrs **uint8, free func()) {
if len(strs) == 0 {
panic("Strs: expected at least 1 string")
}
// Allocate a contiguous array large enough to hold all the strings' contents.
n := 0
for i := range strs {
n += len(strs[i])
}
if n == 0 {
n = 1 // avoid allocating zero bytes in case all strings are empty.
}
data := C.malloc(C.size_t(n))
// Copy all the strings into data.
dataSlice := (*[1 << 30]byte)(data)[:n]
css := make([]*uint8, len(strs)) // Populated with pointers to each string.
offset := 0
for i := range strs {
copy(dataSlice[offset:offset+len(strs[i])], strs[i][:]) // Copy strs[i] into proper data location.
css[i] = (*uint8)(unsafe.Pointer(&dataSlice[offset])) // Set a pointer to it.
offset += len(strs[i])
}
return (**uint8)(&css[0]), func() { C.free(data) }
}
+31
View File
@@ -0,0 +1,31 @@
// Code generated by glow (https://github.com/go-gl/glow). DO NOT EDIT.
package gl
import "C"
import "unsafe"
type DebugProc func(
source uint32,
gltype uint32,
id uint32,
severity uint32,
length int32,
message string,
userParam unsafe.Pointer)
var userDebugCallback DebugProc
//export glowDebugCallback_glcore33
func glowDebugCallback_glcore33(
source uint32,
gltype uint32,
id uint32,
severity uint32,
length int32,
message *uint8,
userParam unsafe.Pointer) {
if userDebugCallback != nil {
userDebugCallback(source, gltype, id, severity, length, GoStr(message), userParam)
}
}
+15867
View File
File diff suppressed because it is too large Load Diff
+75
View File
@@ -0,0 +1,75 @@
// Code generated by glow (https://github.com/go-gl/glow). DO NOT EDIT.
// This file implements GlowGetProcAddress for every supported platform. The
// correct version is chosen automatically based on build tags:
//
// windows: WGL
// darwin: CGL
// linux freebsd netbsd openbsd: GLX
//
// Use of EGL instead of the platform's default (listed above) is made possible
// via the "egl" build tag.
//
// It is also possible to install your own function outside this package for
// retrieving OpenGL function pointers, to do this see InitWithProcAddrFunc.
package gl
/*
#cgo windows CFLAGS: -DTAG_WINDOWS
#cgo !gles2,windows LDFLAGS: -lopengl32
#cgo gles2,windows LDFLAGS: -lGLESv2
#cgo darwin CFLAGS: -DTAG_DARWIN
#cgo !gles2,darwin LDFLAGS: -framework OpenGL
#cgo gles2,darwin LDFLAGS: -framework OpenGLES
#cgo linux freebsd netbsd openbsd CFLAGS: -DTAG_POSIX
#cgo !egl,linux !egl,freebsd !egl,netbsd !egl,openbsd pkg-config: gl
#cgo egl,linux egl,freebsd egl,netbsd egl,openbsd egl,windows CFLAGS: -DTAG_EGL
#cgo egl,linux egl,freebsd egl,netbsd egl,openbsd pkg-config: egl
#cgo egl,windows LDFLAGS: -lEGL
#cgo egl,darwin LDFLAGS: -lEGL
// Check the EGL tag first as it takes priority over the platform's default
// configuration of WGL/GLX/CGL.
#if defined(TAG_EGL)
#include <stdlib.h>
#include <EGL/egl.h>
static void* GlowGetProcAddress(const char* name) {
return eglGetProcAddress(name);
}
#elif defined(TAG_WINDOWS)
#define WIN32_LEAN_AND_MEAN 1
#include <windows.h>
#include <stdlib.h>
static HMODULE ogl32dll = NULL;
static void* GlowGetProcAddress(const char* name) {
void* pf = wglGetProcAddress((LPCSTR) name);
if (pf) {
return pf;
}
if (ogl32dll == NULL) {
ogl32dll = LoadLibraryA("opengl32.dll");
}
return GetProcAddress(ogl32dll, (LPCSTR) name);
}
#elif defined(TAG_DARWIN)
#include <stdlib.h>
#include <dlfcn.h>
static void* GlowGetProcAddress(const char* name) {
return dlsym(RTLD_DEFAULT, name);
}
#elif defined(TAG_POSIX)
#include <stdlib.h>
#include <GL/glx.h>
static void* GlowGetProcAddress(const char* name) {
return glXGetProcAddress((const GLubyte *) name);
}
#endif
*/
import "C"
import "unsafe"
func getProcAddress(namea string) unsafe.Pointer {
cname := C.CString(namea)
defer C.free(unsafe.Pointer(cname))
return C.GlowGetProcAddress(cname)
}
+14
View File
@@ -0,0 +1,14 @@
# This is the official list of go-gl/mathgl authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS files.
# See the latter for an explanation.
# Names should be added to this file as
# Name or Organization <email address>
# The email address is not required for organizations.
# Please keep the list sorted.
Alex Vasi <eee@someuser.com>
Dmitri Shuralyov <shurcooL@gmail.com>
GlenKelley <???>
Zoe Juozapaitis <jragonmiris@gmail.com>
+21
View File
@@ -0,0 +1,21 @@
# This is the official list of people who can contribute
# (and typically have contributed) code to the mathgl
# repository.
#
# The AUTHORS file lists the copyright holders; this file
# lists people. For example, Google employees would be listed here
# but not in AUTHORS, because Google would hold the copyright.
#
# When adding J Random Contributor's name to this file,
# either J's name or J's organization's name should be
# added to the AUTHORS file.
#
# Names should be added to this file like so:
# Name <email address>
#
# Please keep the list sorted.
Alex Vasi <eee@someuser.com>
Dmitri Shuralyov <shurcooL@gmail.com>
GlenKelley <???>
Zoe Juozapaitis <jragonmiris@gmail.com>
+23
View File
@@ -0,0 +1,23 @@
Copyright ©2013 The go-gl Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the gonum project nor the names of its authors and
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+94
View File
@@ -0,0 +1,94 @@
// Copyright 2014 The go-gl Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mgl32
import (
"math"
)
// CartesianToSpherical converts 3-dimensional cartesian coordinates (x,y,z) to spherical
// coordinates with radius r, inclination theta, and azimuth phi.
//
// All angles are in radians.
func CartesianToSpherical(coord Vec3) (r, theta, phi float32) {
r = coord.Len()
theta = float32(math.Acos(float64(coord[2] / r)))
phi = float32(math.Atan2(float64(coord[1]), float64(coord[0])))
return
}
// CartesianToCylindical converts 3-dimensional cartesian coordinates (x,y,z) to
// cylindrical coordinates with radial distance r, azimuth phi, and height z.
//
// All angles are in radians.
func CartesianToCylindical(coord Vec3) (rho, phi, z float32) {
rho = float32(math.Hypot(float64(coord[0]), float64(coord[1])))
phi = float32(math.Atan2(float64(coord[1]), float64(coord[0])))
z = coord[2]
return
}
// SphericalToCartesian converts spherical coordinates with radius r, inclination theta,
// and azimuth phi to cartesian coordinates (x,y,z).
//
// Angles are in radians.
func SphericalToCartesian(r, theta, phi float32) Vec3 {
st, ct := math.Sincos(float64(theta))
sp, cp := math.Sincos(float64(phi))
return Vec3{r * float32(st*cp), r * float32(st*sp), r * float32(ct)}
}
// SphericalToCylindrical converts spherical coordinates with radius r,
// inclination theta, and azimuth phi to cylindrical coordinates with radial
// distance r, azimuth phi, and height z.
//
// Angles are in radians
func SphericalToCylindrical(r, theta, phi float32) (rho, phi2, z float32) {
s, c := math.Sincos(float64(theta))
rho = r * float32(s)
z = r * float32(c)
phi2 = phi
return
}
// CylindircalToSpherical converts cylindrical coordinates with radial distance
// r, azimuth phi, and height z to spherical coordinates with radius r,
// inclination theta, and azimuth phi.
//
// Angles are in radians
func CylindircalToSpherical(rho, phi, z float32) (r, theta, phi2 float32) {
r = float32(math.Hypot(float64(rho), float64(z)))
phi2 = phi
theta = float32(math.Atan2(float64(rho), float64(z)))
return
}
// CylindricalToCartesian converts cylindrical coordinates with radial distance
// r, azimuth phi, and height z to cartesian coordinates (x,y,z)
//
// Angles are in radians.
func CylindricalToCartesian(rho, phi, z float32) Vec3 {
s, c := math.Sincos(float64(phi))
return Vec3{rho * float32(c), rho * float32(s), z}
}
// DegToRad converts degrees to radians
func DegToRad(angle float32) float32 {
return angle * float32(math.Pi) / 180
}
// RadToDeg converts radians to degrees
func RadToDeg(angle float32) float32 {
return angle * 180 / float32(math.Pi)
}

Some files were not shown because too many files have changed in this diff Show More