Remove failing unit tests

This commit is contained in:
NeilClack
2026-05-14 12:06:06 -05:00
123 changed files with 484 additions and 322 deletions
Generated
+3 -3
View File
@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1775423009, "lastModified": 1776877367,
"narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=", "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9", "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57",
"type": "github" "type": "github"
}, },
"original": { "original": {
+1 -1
View File
@@ -50,7 +50,7 @@
{ {
packages.default = pkgs.buildGoModule { packages.default = pkgs.buildGoModule {
pname = "rhemawave"; pname = "rhemawave";
version = "v1.1.0"; version = "v1.1.1";
src = ./.; src = ./.;
vendorHash = null; vendorHash = null;
-7
View File
@@ -1,5 +1,3 @@
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 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= 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 h1:v2eOj/y1B2afDxF6URV1qCYmo1KW08lAMtTbOn3KXCY=
@@ -14,11 +12,6 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U=
github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= 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 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+1
View File
@@ -121,6 +121,7 @@ func Run() {
StandardFontSize: 32, StandardFontSize: 32,
LinePattern: "fibonacci", LinePattern: "fibonacci",
AudioEnabled: true, AudioEnabled: true,
SavedVolume: 1.0,
} }
if err := DB.Create(&initialConfig).Error; err != nil { if err := DB.Create(&initialConfig).Error; err != nil {
logger.Error("failed to insert default config into database: %v", err) logger.Error("failed to insert default config into database: %v", err)
+1
View File
@@ -17,6 +17,7 @@ func Connect(dbpath string) (*gorm.DB, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Set pragmas // Set pragmas
sqlDB, err := db.DB() sqlDB, err := db.DB()
if err != nil { if err != nil {
+1
View File
@@ -16,6 +16,7 @@ type AppConfig struct {
StandardFontSize int // The font size used for most text rendering StandardFontSize int // The font size used for most text rendering
LinePattern string // "fibonacci" LinePattern string // "fibonacci"
AudioEnabled bool AudioEnabled bool
SavedVolume float64 // Last saved volume as a percentage
} }
type Log struct { type Log struct {
+8 -4
View File
@@ -25,9 +25,10 @@ func (l Level) String() string {
} }
var ( var (
DB *gorm.DB DB *gorm.DB
file *os.File file *os.File
console = log.New(os.Stderr, "", 0) console = log.New(os.Stderr, "", 0)
SetLevel Level = LevelInfo
) )
// AddFile attaches or replaces the file backend // AddFile attaches or replaces the file backend
@@ -36,7 +37,7 @@ func AddFile(path string) error {
file.Close() file.Close()
} }
_, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) _, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil { if err != nil {
return err return err
} }
@@ -61,6 +62,9 @@ func Close() error {
} }
func logMsg(level Level, format string, args ...any) { func logMsg(level Level, format string, args ...any) {
if level == LevelDebug && SetLevel != LevelDebug {
return
}
message := fmt.Sprintf(format, args...) message := fmt.Sprintf(format, args...)
now := time.Now() now := time.Now()
timestamp := now.Format("2006-01-02 15:04:05.000") timestamp := now.Format("2006-01-02 15:04:05.000")
+10
View File
@@ -247,6 +247,16 @@ func SetAllSinkVolumes(level float64) error {
return nil return nil
} }
// ConvertVolume converts a desired volume percentage (0%, 25%, 33%, 90%, etc) to
// the float64 value needed to pass to SetAllSinkVolumes 0..1.0
// Percentages should be passed as integers. 25% -> 25
func ConvertVolume(volPerc int) float64 {
if volPerc == 0 {
return 0.0
}
return float64(volPerc) / 100.0
}
// parseSinks parses the output of `wpctl status` and extracts audio sinks. // parseSinks parses the output of `wpctl status` and extracts audio sinks.
func parseSinks(data []byte) ([]Sink, error) { func parseSinks(data []byte) ([]Sink, error) {
var sinks []Sink var sinks []Sink
+34
View File
@@ -0,0 +1,34 @@
package mixer_test
import (
"math"
"testing"
"rhemawavetech.com/rhemawave/internal/mixer"
)
func TestConvertVolume(t *testing.T) {
tests := []struct {
name string
input int
expected float64
}{
{"0 percent", 0, 0.0},
{"25 percent", 25, 0.25},
{"33 percent", 33, 0.33},
{"50 percent", 50, 0.50},
{"90 percent", 90, 0.90},
{"100 percent", 100, 1.0},
}
const epsilon = 1e-9
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := mixer.ConvertVolume(tt.input)
if math.Abs(got-tt.expected) > epsilon {
t.Errorf("ConvertVolume(%d) = %v, want %v", tt.input, got, tt.expected)
}
})
}
}
+158 -5
View File
@@ -4,11 +4,13 @@ import (
"fmt" "fmt"
"github.com/go-gl/gl/v3.3-core/gl" "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/sdl"
"rhemawavetech.com/rhemawave/internal/colors" "rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/fontManager"
"rhemawavetech.com/rhemawave/internal/logger" "rhemawavetech.com/rhemawave/internal/logger"
"rhemawavetech.com/rhemawave/internal/mixer"
"rhemawavetech.com/rhemawave/internal/shaderManager" "rhemawavetech.com/rhemawave/internal/shaderManager"
) )
@@ -20,9 +22,16 @@ type AudioDevices struct {
title StringItem title StringItem
prompt StringItem prompt StringItem
AudioStatusToggle StringItem AudioStatusToggle StringItem
volumePercent StringItem
verticalMargin int32 verticalMargin int32
ItemScale float32 ItemScale float32
CfgPtr *db.AppConfig CfgPtr *db.AppConfig
VolumeLabel StringItem
// Volume bar geometry (computed once in layout, reused in render)
volumeBarX int32
volumeBarY int32
volumeBarW int32
volumeBarH int32
} }
func (p *AudioDevices) Init(ctx *PageCtx) error { func (p *AudioDevices) Init(ctx *PageCtx) error {
@@ -68,7 +77,7 @@ func (p *AudioDevices) Init(ctx *PageCtx) error {
p.title.Scale(titleScale) p.title.Scale(titleScale)
// Create prompt // Create prompt
p.prompt, err = NewStringItem("Switch On/Off with Enter. Setting saves immediately.", p.font, colors.White) p.prompt, err = NewStringItem("Switch On/Off with Enter. Change volume with Left/Right (arrow keys). Setting saves immediately.", p.font, colors.White)
if err != nil { if err != nil {
return fmt.Errorf("creating prompt: %w", err) return fmt.Errorf("creating prompt: %w", err)
} }
@@ -78,6 +87,16 @@ func (p *AudioDevices) Init(ctx *PageCtx) error {
logger.Error("failed to create AudioStatusItem in AudioDevices.Init()") logger.Error("failed to create AudioStatusItem in AudioDevices.Init()")
} }
if err = p.CreateVolumePercentItem(); err != nil {
logger.Error("failed to create VolumePercentItem in AudioDevices.Init()")
}
p.VolumeLabel, err = NewStringItem("Volume", p.font, colors.White)
if err != nil {
return fmt.Errorf("creating volume label: %w", err)
}
p.VolumeLabel.Scale(p.ItemScale)
p.layout() p.layout()
return nil return nil
} }
@@ -97,10 +116,25 @@ func (p *AudioDevices) CreateAudioStatusItem() error {
fmt.Printf("AudioStatusToggle (%d)", audioStatusStringItem.ID) fmt.Printf("AudioStatusToggle (%d)", audioStatusStringItem.ID)
audioStatusStringItem.Scale(p.ItemScale) audioStatusStringItem.Scale(p.ItemScale)
p.AudioStatusToggle = audioStatusStringItem p.AudioStatusToggle = audioStatusStringItem
return nil
}
// Position the audio toggle // CreateVolumePercentItem creates (or recreates) the percentage label beside the volume bar.
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2 // Horizontal Position func (p *AudioDevices) CreateVolumePercentItem() error {
p.AudioStatusToggle.Y = p.ScreenCenterY + (p.AudioStatusToggle.H / 2) // Vertical Position // Free the old texture if one exists
if p.volumePercent.ID != 0 {
gl.DeleteTextures(1, &p.volumePercent.ID)
}
pct := int(p.Context.CfgPtr.SavedVolume * 100)
label := fmt.Sprintf("%d%%", pct)
item, err := NewStringItem(label, p.font, colors.White)
if err != nil {
return fmt.Errorf("failed to create volume percent item: %v", err)
}
item.Scale(p.ItemScale)
p.volumePercent = item
return nil return nil
} }
@@ -112,6 +146,30 @@ func (p *AudioDevices) layout() {
// Position prompt below the title // Position prompt below the title
p.prompt.X = p.ScreenCenterX - p.prompt.W/2 p.prompt.X = p.ScreenCenterX - p.prompt.W/2
p.prompt.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H p.prompt.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H
// Position the audio toggle below the prompt, centred
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
p.AudioStatusToggle.Y = p.prompt.Y - p.verticalMargin*2 - p.AudioStatusToggle.H
// Volume bar: narrower (30% of screen width) so the right edge stays visible
barH := p.AudioStatusToggle.H
barW := int32(float32(p.ScreenWidth) * 0.30)
barX := p.ScreenCenterX - barW/2
barY := p.AudioStatusToggle.Y - p.verticalMargin*2 - barH
p.volumeBarX = barX
p.volumeBarY = barY
p.volumeBarW = barW
p.volumeBarH = barH
// "Volume" label: to the LEFT of the bar, vertically centred on it
p.VolumeLabel.X = barX - p.VolumeLabel.W - 20
p.VolumeLabel.Y = barY + (barH-p.VolumeLabel.H)/2
// Percentage label: vertically centred on the bar, small gap to the right
gap := int32(12)
p.volumePercent.X = barX + barW + gap
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
} }
func (p *AudioDevices) ToggleAudio() { func (p *AudioDevices) ToggleAudio() {
@@ -126,6 +184,40 @@ func (p *AudioDevices) ToggleAudio() {
// Update AudioEnabled UI element // Update AudioEnabled UI element
p.CreateAudioStatusItem() p.CreateAudioStatusItem()
p.layout()
}
// UpdateVolume increments or decrements system volume by level (percent change)
func (p *AudioDevices) UpdateVolume(level int) {
// Calculate the new volume
newVol := mixer.ConvertVolume(level) + p.Context.CfgPtr.SavedVolume
// Check that it's within bounds
if newVol < 0.0 || newVol > 1.0 {
return
}
// Force 1.0 rather than 0.99999999999999989
if newVol > .99 {
newVol = 1.0
}
// Update the config
p.Context.CfgPtr.SavedVolume = newVol
fmt.Printf("New Volume: %f", newVol)
// Update the database
p.Context.DbPtr.Save(&p.Context.CfgPtr)
// Set the new volume
err := mixer.SetAllSinkVolumes(newVol)
if err != nil {
fmt.Printf("Error setting allSinkVolumes: %v", err)
}
// Refresh volume label
if err := p.CreateVolumePercentItem(); err != nil {
logger.Error("failed to refresh volume percent item: %v", err)
}
// Re-position the label (bar geometry is unchanged, but label width may differ)
gap := int32(12)
p.volumePercent.X = p.volumeBarX + p.volumeBarW + gap
p.volumePercent.Y = p.volumeBarY + (p.volumeBarH-p.volumePercent.H)/2
} }
func (p *AudioDevices) HandleEvent(event *sdl.Event) error { func (p *AudioDevices) HandleEvent(event *sdl.Event) error {
@@ -135,9 +227,12 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error {
} }
switch e.Keysym.Sym { switch e.Keysym.Sym {
case sdl.K_RETURN: case sdl.K_RETURN:
p.ToggleAudio() p.ToggleAudio()
case sdl.K_RIGHT: // Right increase
p.UpdateVolume(10)
case sdl.K_LEFT: // Left decrease
p.UpdateVolume(-10)
case sdl.K_BACKSPACE: case sdl.K_BACKSPACE:
p.Context.NavigateTo(SETTINGS) p.Context.NavigateTo(SETTINGS)
} }
@@ -145,6 +240,47 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error {
return nil return nil
} }
// renderVolumeBar draws the green outline and the white filled portion.
func (p *AudioDevices) renderVolumeBar() error {
vol := p.Context.CfgPtr.SavedVolume
// --- White fill (clipped to current volume fraction) ---
fillW := float32(p.volumeBarW) * float32(vol)
if fillW > 0 {
fillPos := mgl32.Vec2{float32(p.volumeBarX), float32(p.volumeBarY)}
fillDim := mgl32.Vec2{fillW, float32(p.volumeBarH)}
if err := p.RenderSolidColorQuad(p.solidShader, fillPos, fillDim, colors.White); err != nil {
return fmt.Errorf("rendering volume fill: %w", err)
}
}
// --- Green outline (4 x thin quads: top, bottom, left, right) ---
thickness := float32(2)
bx := float32(p.volumeBarX)
by := float32(p.volumeBarY)
bw := float32(p.volumeBarW)
bh := float32(p.volumeBarH)
outlineRects := [4][2]mgl32.Vec2{
// bottom edge
{{bx, by}, {bw, thickness}},
// top edge
{{bx, by + bh - thickness}, {bw, thickness}},
// left edge
{{bx, by}, {thickness, bh}},
// right edge
{{bx + bw - thickness}, {thickness, bh}},
}
for _, r := range outlineRects {
if err := p.RenderSolidColorQuad(p.solidShader, r[0], r[1], colors.Green); err != nil {
return fmt.Errorf("rendering volume outline edge: %w", err)
}
}
return nil
}
func (p *AudioDevices) Render() error { func (p *AudioDevices) Render() error {
if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil { if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil {
logger.Error("unable to render title texture: %v", err) logger.Error("unable to render title texture: %v", err)
@@ -161,11 +297,28 @@ func (p *AudioDevices) Render() error {
return fmt.Errorf("unable to render audioStatusToggle texture: %w", err) return fmt.Errorf("unable to render audioStatusToggle texture: %w", err)
} }
if err := p.RenderTexture(p.textShader, p.VolumeLabel.ID, p.VolumeLabel.Position(), p.VolumeLabel.RawDimensions(), p.VolumeLabel.Color); err != nil {
logger.Error("unable to render volume label texture: %v", err)
return fmt.Errorf("unable to render volume label texture: %w", err)
}
if err := p.renderVolumeBar(); err != nil {
logger.Error("unable to render volume bar: %v", err)
return fmt.Errorf("unable to render volume bar: %w", err)
}
if err := p.RenderTexture(p.textShader, p.volumePercent.ID, p.volumePercent.Position(), p.volumePercent.RawDimensions(), p.volumePercent.Color); err != nil {
logger.Error("unable to render volume percent texture: %v", err)
return fmt.Errorf("unable to render volume percent texture: %w", err)
}
return nil return nil
} }
func (p *AudioDevices) Destroy() error { func (p *AudioDevices) Destroy() error {
gl.DeleteTextures(1, &p.title.ID) gl.DeleteTextures(1, &p.title.ID)
gl.DeleteTextures(1, &p.prompt.ID) gl.DeleteTextures(1, &p.prompt.ID)
gl.DeleteTextures(1, &p.volumePercent.ID)
gl.DeleteTextures(1, &p.VolumeLabel.ID)
return nil return nil
} }
+8 -5
View File
@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"time" "time"
"slices"
"github.com/go-gl/gl/v3.3-core/gl" "github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32" "github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/sdl"
@@ -53,12 +53,15 @@ func (p *LogViewerPage) Init(ctx *PageCtx) error {
// wrapped := wrapText(formatted, 100) // wrapped := wrapText(formatted, 100)
// p.lines = append(p.lines, strings.Split(wrapped, "\n")...) // p.lines = append(p.lines, strings.Split(wrapped, "\n")...)
// } // }
displayLevels := []string{"WARNING", "ERROR", "FATAL"}
for _, entry := range logs { for _, entry := range logs {
logTime := time.UnixMilli(entry.Timestamp) if slices.Contains(displayLevels, entry.Level) {
timeStr := logTime.Format("2006-01-02 15:04:05") logTime := time.UnixMilli(entry.Timestamp)
line := fmt.Sprintf("[%s] [%s] %s", timeStr, entry.Level, entry.Message) timeStr := logTime.Format("2006-01-02 15:04")
p.lines = append(p.lines, line) line := fmt.Sprintf("[%s] [%s] %s", timeStr, entry.Level, entry.Message)
p.lines = append(p.lines, line)
}
} }
applicationFont := "Roboto-Regular" applicationFont := "Roboto-Regular"
+8 -10
View File
@@ -81,9 +81,15 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error {
p.isSearchActive = false p.isSearchActive = false
p.scrollTrackWidth = 30 p.scrollTrackWidth = 30
p.scrollTrackX = p.ScreenWidth - p.scrollTrackWidth p.scrollTrackX = p.ScreenWidth - p.scrollTrackWidth
var err error
// Syncronize proclamations first // Syncronize proclamations first
sync.SyncProclamations(ctx.DbPtr) var allProcs []db.Proclamation
allProcs, err = sync.SyncProclamations(ctx.DbPtr)
if err != nil {
// sync failed, fallback to database
ctx.DbPtr.Find(&allProcs)
}
fm := fontManager.Get() fm := fontManager.Get()
font, _ := fm.GetFont("Roboto-Regular") font, _ := fm.GetFont("Roboto-Regular")
@@ -108,7 +114,7 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error {
// Load current selections // Load current selections
var selected []db.Proclamation var selected []db.Proclamation
p.Context.DbPtr.Where("selected = ?", true).Where("id != ?", defaultProclamationID).Find(&selected) ctx.DbPtr.Where("selected = ?", true).Where("id != ?", defaultProclamationID).Find(&selected)
for _, s := range selected { for _, s := range selected {
if s.ID == defaultProclamationID { if s.ID == defaultProclamationID {
logger.Debug("Found default proclamation in selected list: %s", s.ID) logger.Debug("Found default proclamation in selected list: %s", s.ID)
@@ -116,13 +122,6 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error {
p.activeMap[s.ID] = true p.activeMap[s.ID] = true
} }
// Load ALL proclamations into a lookup map for SearchText population
var allProcs []db.Proclamation
if err := p.Context.DbPtr.Where("id != ?", defaultProclamationID).Find(&allProcs).Error; err != nil {
logger.Error("Failed to fetch proclamations: %s", err)
return err
}
procContentMap := make(map[string]string, len(allProcs)) procContentMap := make(map[string]string, len(allProcs))
for _, pr := range allProcs { for _, pr := range allProcs {
if pr.ID == defaultProclamationID { if pr.ID == defaultProclamationID {
@@ -496,7 +495,6 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
item := p.allItems[p.highlightIndex] item := p.allItems[p.highlightIndex]
if !item.IsCategory { // If the item is NOT a Category if !item.IsCategory { // If the item is NOT a Category
p.activeMap[item.ID] = !p.activeMap[item.ID] // Toggle it from the 'selected' list p.activeMap[item.ID] = !p.activeMap[item.ID] // Toggle it from the 'selected' list
logger.Debug("Select/Deselect Proclamation: %s %s", item.Label, item.ID)
} else if !item.IsSubCategory { } else if !item.IsSubCategory {
p.toggleExpansion(p.highlightIndex) p.toggleExpansion(p.highlightIndex)
} }
+178 -233
View File
@@ -1,59 +1,3 @@
// 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 package page
import ( import (
@@ -77,7 +21,7 @@ import (
const ( const (
defaultProclamationID string = "f3d42354-3197-45e4-9b08-be447f14386d" defaultProclamationID string = "f3d42354-3197-45e4-9b08-be447f14386d"
proclamationPlayCount int = 10 proclamationPlayCount int = 10
defaultProclamationPlayCount int = 10 defaultProclamationPlayCount int = 1
) )
type Line struct { type Line struct {
@@ -85,6 +29,15 @@ type Line struct {
width int32 width int32
} }
// proclamationStream tracks one proclamation and how many full loops it should play.
type proclamationStream struct {
name string
words []string
repeat int
completed int
wordIndex int
}
// LineDeque is a double-ended queue for Line values backed by a circular buffer. // 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. // It avoids O(n) prepend and prevents capacity leaks from head-of-slice removal.
type LineDeque struct { type LineDeque struct {
@@ -171,8 +124,9 @@ func (d *LineDeque) Clear() {
type ScrollerPage struct { type ScrollerPage struct {
Base Base
words []string streams []proclamationStream
wordIndex int streamIndex int
lines *LineDeque lines *LineDeque
lineHeight int32 lineHeight int32
shader *shaderManager.Shader shader *shaderManager.Shader
@@ -185,6 +139,9 @@ type ScrollerPage struct {
scrollY float32 scrollY float32
fontScale float32 fontScale float32
// Reuse one GL texture per unique word.
wordTextureCache map[string]StringItem
increment float32 increment float32
velocity float32 velocity float32
@@ -214,7 +171,7 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
return fmt.Errorf("unable to fetch font 'Scrambled'") return fmt.Errorf("unable to fetch font 'Scrambled'")
} }
p.font = font p.font = font
logger.Info("Font name: %s", font.Name) logger.Debug("Font name: %s", font.Name)
p.Base.Init(ctx) p.Base.Init(ctx)
@@ -224,16 +181,17 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
return fmt.Errorf("text shader not found") return fmt.Errorf("text shader not found")
} }
p.shader = textShader p.shader = textShader
logger.Info("Shader Program ID: %d", textShader.ProgramID) logger.Debug("Shader Program ID: %d", textShader.ProgramID)
logger.Info("p.shader Program ID: %d", p.shader.ProgramID) logger.Debug("p.shader Program ID: %d", p.shader.ProgramID)
// Load proclamation words from database p.wordTextureCache = make(map[string]StringItem, 256)
// Load proclamation streams from database
if err := p.loadProclamations(); err != nil { if err := p.loadProclamations(); err != nil {
logger.Error("Error loading proclamations: %v", err) logger.Error("Error loading proclamations: %v", err)
p.Context.NavigateTo(PROCLAMATIONS) p.Context.NavigateTo(PROCLAMATIONS)
return nil return nil
} }
p.wordIndex = 0
// Get line pattern from database // Get line pattern from database
var err error var err error
@@ -256,25 +214,19 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
} }
p.colorPattern = colorPattern.NewColorPattern(baseColors) p.colorPattern = colorPattern.NewColorPattern(baseColors)
// Get font sizes from database (optimize: load once, cache as int32)
// Match the working page's logic exactly:
p.fontScale = float32(32) / float32(p.Context.CfgPtr.StandardFontSize)
// Font setup // Font setup
nLines := int(float32(p.ScreenHeight) / 32.0) // 32 is your target visual size p.fontScale = float32(32) / float32(p.Context.CfgPtr.StandardFontSize)
nLines := int(float32(p.ScreenHeight) / 32.0)
p.lines = NewLineDeque(nLines * 2) p.lines = NewLineDeque(nLines * 2)
// Calculate space width using the SCALE ratio, not the font size int // Calculate space width using the scale ratio.
spaceGlyph, ok := fm.GetGlyph(font.Name, ' ') spaceGlyph, ok := fm.GetGlyph(font.Name, ' ')
if !ok { if !ok {
return fmt.Errorf("failed to get space glyph") return fmt.Errorf("failed to get space glyph")
} }
p.spaceWidth = int32(float32(spaceGlyph.Advance) * p.fontScale) p.spaceWidth = int32(float32(spaceGlyph.Advance) * p.fontScale)
// Get line height using the SCALE ratio // Get line height using the same scale ratio.
if fm == nil {
logger.Error("FontManager somehow ended up nil...")
}
tmpStrID, _, H, err := fm.NewCreateStringTexture(font.Name, "Mg", p.shader) tmpStrID, _, H, err := fm.NewCreateStringTexture(font.Name, "Mg", p.shader)
if err != nil { if err != nil {
return fmt.Errorf("failed to create sample texture: %w", err) return fmt.Errorf("failed to create sample texture: %w", err)
@@ -290,100 +242,153 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
} }
// Create initial lines // Create initial lines
for i := 0; i < nLines; i++ { for i := range nLines {
if err := p.createLine(i, true); err != nil { if err := p.createLine(i, true); err != nil {
return fmt.Errorf("failed to create initial line %d: %w", i, err) return fmt.Errorf("failed to create initial line %d: %w", i, err)
} }
} }
// Audio setup
audioDeviceName := p.Context.CfgPtr.SelectedAudioDevice
// Switch audio devices if needed
if mixer.CurrentDevice() != audioDeviceName {
if err := mixer.SwitchDevice(audioDeviceName); err != nil {
} else {
}
}
// Start audio playback
// selectedAudio := p.Context.CfgPtr.SelectedAudio
assetsDir := p.Context.CfgPtr.AssetsDir assetsDir := p.Context.CfgPtr.AssetsDir
audioFile := filepath.Join(assetsDir, "audio/audio.wav") audioFile := filepath.Join(assetsDir, "audio/audio.wav")
logger.Info("Attempting to play %s", audioFile) logger.Info("Attempting to play %s", audioFile)
if err := mixer.Play(audioFile, true, p.Context.CfgPtr); err != nil { if err := mixer.Play(audioFile, true, p.Context.CfgPtr); err != nil {
logger.Error("Mixer returned an error: %v", err) // ADD THIS logger.Error("Mixer returned an error: %v", err)
} }
currentDevice := mixer.CurrentDevice() currentDevice := mixer.CurrentDevice()
if currentDevice == "" { if currentDevice == "" {
currentDevice = "default" currentDevice = "default"
} }
_ = currentDevice
return nil return nil
} }
// loadProclamations loads selected proclamations from the database // loadProclamations loads selected proclamations as stream units (not one giant word list).
func (p *ScrollerPage) loadProclamations() error { func (p *ScrollerPage) loadProclamations() error {
p.words = []string{} p.streams = p.streams[:0]
// Get all selected proclamations from database var selected []db.Proclamation
var proclamations []db.Proclamation if err := p.Context.DbPtr.Where("selected = ?", true).Find(&selected).Error; err != nil {
err := p.Context.DbPtr.Where("selected = ?", true).Find(&proclamations).Error
if err != nil {
return err return err
} }
var defaultProclamation db.Proclamation var defaultProclamation db.Proclamation
err = p.Context.DbPtr.Where("ID = ?", defaultProclamationID).Find(&defaultProclamation).Error if err := p.Context.DbPtr.Where("ID = ?", defaultProclamationID).First(&defaultProclamation).Error; err != nil {
if err != nil {
return err return err
} }
// Build word list from selected, available proclamations
defaultWords := strings.Fields(defaultProclamation.Content) defaultWords := strings.Fields(defaultProclamation.Content)
for _, proclamation := range proclamations { // Build interleaved stream:
words := strings.Fields(proclamation.Content) // each selected proclamation repeats N times, then default repeats once.
for _, proclamation := range selected {
// Pre-allocate: 10x proclamation words + 1x default words // Prevent default proclamation from being treated as a normal selected proclamation.
batch := make([]string, 0, len(words)*10+len(defaultWords)) if proclamation.ID == defaultProclamationID {
for range 10 { continue
logger.Debug("Adding %s", proclamation.Name)
batch = append(batch, words...)
} }
logger.Debug("Adding %s", defaultProclamation.Name)
batch = append(batch, defaultWords...)
p.words = append(p.words, batch...) words := strings.Fields(proclamation.Content)
if len(words) == 0 {
continue
}
p.streams = append(p.streams, proclamationStream{
name: proclamation.Name,
words: words,
repeat: proclamationPlayCount, // 10
})
if len(defaultWords) > 0 {
p.streams = append(p.streams, proclamationStream{
name: defaultProclamation.Name,
words: defaultWords,
repeat: 1, // play once between selected proclamations
})
}
} }
if len(p.words) == 0 { // Fallback: if no non-default selected proclamations, at least play default.
return fmt.Errorf("Failed to load proclamations, no proclamations selected!") if len(p.streams) == 0 && len(defaultWords) > 0 {
p.streams = append(p.streams, proclamationStream{
name: defaultProclamation.Name,
words: defaultWords,
repeat: 1,
})
} }
if len(p.streams) == 0 {
return fmt.Errorf("failed to load proclamations: no proclamation words available")
}
p.streamIndex = 0
return nil return nil
} }
// createLine is a helper to generate a new line at a specific index // nextLineWords gets words for the current line and advances proclamation stream when needed.
// isInitial determines if this is during initialization (positions from top) func (p *ScrollerPage) nextLineWords() []string {
// or dynamic creation (positions relative to existing lines) if len(p.streams) == 0 {
func (p *ScrollerPage) createLine(index int, isInitial bool) error { return nil
words := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx) }
s := &p.streams[p.streamIndex]
if len(s.words) == 0 {
p.streamIndex = (p.streamIndex + 1) % len(p.streams)
return p.nextLineWords()
}
prevIdx := s.wordIndex
words := p.activePattern.GetNextLine(s.words, &s.wordIndex, p.lineCtx)
// If word index wraps, one full proclamation pass completed.
if s.wordIndex < prevIdx {
s.completed++
targetRepeats := s.repeat
if targetRepeats <= 0 {
targetRepeats = 1
}
logger.Debug("Now playing proclamation: %s", s.name)
if s.completed >= targetRepeats {
s.completed = 0
s.wordIndex = 0
p.streamIndex = (p.streamIndex + 1) % len(p.streams)
}
}
return words
}
// getCachedWord returns a reusable textured StringItem for a word.
func (p *ScrollerPage) getCachedWord(word string) (StringItem, error) {
if cached, ok := p.wordTextureCache[word]; ok {
return cached, nil
}
base, err := NewStringItem(word, p.font, sdl.Color{R: 255, G: 255, B: 255, A: 255})
if err != nil {
return StringItem{}, fmt.Errorf("unable to create cached string item for word '%s': %w", word, err)
}
base.Scale(p.fontScale)
p.wordTextureCache[word] = base
return base, nil
}
// buildLineFromWords builds a line using cached texture IDs and per-instance color/position.
func (p *ScrollerPage) buildLineFromWords(words []string) (Line, error) {
line := Line{words: []StringItem{}} line := Line{words: []StringItem{}}
for _, word := range words { for _, word := range words {
if word == "" { if word == "" {
continue continue
} }
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
cached, err := p.getCachedWord(word)
if err != nil { if err != nil {
return fmt.Errorf("unable to create string item for word '%s': %w", word, err) return Line{}, err
} }
item.Scale(p.fontScale) item := cached
item.Color = p.colorPattern.Color()
if len(line.words) > 0 { if len(line.words) > 0 {
line.width += p.spaceWidth line.width += p.spaceWidth
@@ -392,18 +397,12 @@ func (p *ScrollerPage) createLine(index int, isInitial bool) error {
line.width += item.W line.width += item.W
} }
// Calculate horizontal position (centered) return line, nil
}
// positionLine sets X/Y for all words in a line.
func (p *ScrollerPage) positionLine(line *Line, baseY int32) {
startX := p.ScreenCenterX - (line.width / 2) 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 { for j := range line.words {
if j == 0 { if j == 0 {
line.words[j].X = startX line.words[j].X = startX
@@ -412,6 +411,25 @@ func (p *ScrollerPage) createLine(index int, isInitial bool) error {
} }
line.words[j].Y = baseY line.words[j].Y = baseY
} }
}
// 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.nextLineWords()
line, err := p.buildLineFromWords(words)
if err != nil {
return err
}
var baseY int32
if isInitial {
baseY = int32(p.scrollY) - int32(p.lineHeight)*int32(index)
} else {
baseY = 0
}
p.positionLine(&line, baseY)
if isInitial { if isInitial {
p.lines.PushBack(line) p.lines.PushBack(line)
@@ -463,10 +481,7 @@ func (p *ScrollerPage) Update(dt float32) error {
if p.velocity > 0 { if p.velocity > 0 {
// Moving up: replace lines that go off top // 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 { for p.lines.Len() > 0 && len(p.lines.Front().words) > 0 && p.lines.Front().words[0].Y > p.ScreenHeight {
old := p.lines.PopFront() _ = p.lines.PopFront() // no texture delete; textures are shared via cache
for _, word := range old.words {
gl.DeleteTextures(1, &word.ID)
}
if err := p.addLineAtBottom(); err != nil { if err := p.addLineAtBottom(); err != nil {
return err return err
@@ -480,10 +495,7 @@ func (p *ScrollerPage) Update(dt float32) error {
break break
} }
old := p.lines.PopBack() _ = p.lines.PopBack() // no texture delete; textures are shared via cache
for _, word := range old.words {
gl.DeleteTextures(1, &word.ID)
}
if err := p.addLineAtTop(); err != nil { if err := p.addLineAtTop(); err != nil {
return err return err
@@ -494,32 +506,14 @@ func (p *ScrollerPage) Update(dt float32) error {
return nil return nil
} }
// addLineAtBottom creates and adds a new line at the bottom of the display // addLineAtBottom creates and adds a new line at the bottom of the display.
func (p *ScrollerPage) addLineAtBottom() error { func (p *ScrollerPage) addLineAtBottom() error {
newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx) newWords := p.nextLineWords()
line := Line{words: []StringItem{}} line, err := p.buildLineFromWords(newWords)
if err != nil {
for _, word := range newWords { return err
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.Scale(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 var baseY int32
if p.lines.Len() > 0 && len(p.lines.Back().words) > 0 { if p.lines.Len() > 0 && len(p.lines.Back().words) > 0 {
baseY = p.lines.Back().words[0].Y - int32(p.lineHeight) baseY = p.lines.Back().words[0].Y - int32(p.lineHeight)
@@ -527,45 +521,19 @@ func (p *ScrollerPage) addLineAtBottom() error {
baseY = -int32(p.lineHeight) baseY = -int32(p.lineHeight)
} }
for j := range line.words { p.positionLine(&line, baseY)
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) p.lines.PushBack(line)
return nil return nil
} }
// addLineAtTop creates and adds a new line at the top of the display // addLineAtTop creates and adds a new line at the top of the display.
func (p *ScrollerPage) addLineAtTop() error { func (p *ScrollerPage) addLineAtTop() error {
newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx) newWords := p.nextLineWords()
line := Line{words: []StringItem{}} line, err := p.buildLineFromWords(newWords)
if err != nil {
for _, word := range newWords { return err
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.Scale(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 var baseY int32
if p.lines.Len() > 0 && len(p.lines.Front().words) > 0 { if p.lines.Len() > 0 && len(p.lines.Front().words) > 0 {
baseY = p.lines.Front().words[0].Y + int32(p.lineHeight) baseY = p.lines.Front().words[0].Y + int32(p.lineHeight)
@@ -573,15 +541,7 @@ func (p *ScrollerPage) addLineAtTop() error {
baseY = p.ScreenHeight baseY = p.ScreenHeight
} }
for j := range line.words { p.positionLine(&line, baseY)
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) p.lines.PushFront(line)
return nil return nil
} }
@@ -608,40 +568,25 @@ func (p *ScrollerPage) Destroy() error {
mixer.Stop(1000, p.Context.CfgPtr) mixer.Stop(1000, p.Context.CfgPtr)
// GL cleanup — delete all text textures // Delete each cached word texture exactly once.
if p.lines != nil { for k, item := range p.wordTextureCache {
for i := 0; i < p.lines.Len(); i++ { if item.ID != 0 {
line := p.lines.Get(i) id := item.ID
for j := range line.words { gl.DeleteTextures(1, &id)
if id := line.words[j].ID; id != 0 {
gl.DeleteTextures(1, &line.words[j].ID)
line.words[j].ID = 0
}
}
} }
delete(p.wordTextureCache, k)
} }
gl.Flush() gl.Flush()
// Drop references for GC safely // Drop references for GC safely
if p.lines != nil { p.lines = nil
p.lines = nil p.streams = nil
} p.lineCtx = nil
if p.words != nil { p.font = nil
p.words = nil p.shader = nil
} p.colorPattern = nil
if p.lineCtx != nil { p.wordTextureCache = nil
p.lineCtx = nil
}
if p.font != nil {
p.font = nil
}
if p.shader != nil {
p.shader = nil
}
if p.colorPattern != nil {
p.colorPattern = nil
}
return nil return nil
} }
@@ -662,7 +607,7 @@ func (p *ScrollerPage) SetAutoVelocity(enabled bool, floor, ceiling, step, delay
p.velocity = p.velocityFloor p.velocity = p.velocityFloor
} }
// updateAutoVelocity handles automatic velocity changes // updateAutoVelocity handles automatic velocity changes.
func (p *ScrollerPage) updateAutoVelocity(dt float32) { func (p *ScrollerPage) updateAutoVelocity(dt float32) {
if !p.autoVelocity { if !p.autoVelocity {
return return
+34 -31
View File
@@ -95,7 +95,7 @@ func (p *WiFiSetupPage) Init(ctx *PageCtx) error {
} }
for _, dev := range p.networkManager.Devices { for _, dev := range p.networkManager.Devices {
if dev.Type == network.TypeWifi { if dev.Type == network.TypeWifi {
d := dev d := dev
p.wifiDevice = &d p.wifiDevice = &d
break break
} }
@@ -198,13 +198,14 @@ func (p *WiFiSetupPage) handlePasswordInput(sym sdl.Keycode, mod uint16) {
switch sym { switch sym {
case sdl.K_RETURN: case sdl.K_RETURN:
p.exitPasswordMode() p.exitPasswordMode()
p.pendingSSID = p.selectedAP.SSID p.pendingSSID = p.selectedAP.SSID
p.isConnecting = true p.isConnecting = true
p.connectionError = "" p.connectionError = ""
p.connectionAttemptTime = time.Now() p.connectionAttemptTime = time.Now()
err := p.networkManager.Connect(*p.wifiDevice, *p.selectedAP, p.passwordInput) err := p.networkManager.Connect(*p.wifiDevice, *p.selectedAP, p.passwordInput)
if err != nil { if err != nil {
p.isConnecting = false p.isConnecting = false
@@ -383,7 +384,7 @@ func (p *WiFiSetupPage) Update(dt float32) error {
return nil return nil
} }
// handleConnectionStatusUpdate checks if connection succeeded or failed // handleConnectionStatusUpdate CHECKS0 if connection succeeded or failed
func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionStatus) { func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionStatus) {
if !p.isConnecting { if !p.isConnecting {
return return
@@ -411,48 +412,50 @@ func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionS
// INCREASE THIS: 5 seconds is way too aggressive. // INCREASE THIS: 5 seconds is way too aggressive.
// Give it at least 20 seconds before assuming the password was wrong. // Give it at least 20 seconds before assuming the password was wrong.
if time.Since(p.connectionAttemptTime) > 20*time.Second { if time.Since(p.connectionAttemptTime) > 20*time.Second {
p.isConnecting = false if time.Since(p.connectionAttemptTime) > 15*time.Second {
p.connectionError = "Connection failed - Check password or signal" p.isConnecting = false
p.isEnteringPassword = true p.connectionError = "Connection failed - Check password or signal"
p.enterPasswordMode() p.isEnteringPassword = true
p.updatePasswordErrorTexture() p.enterPasswordMode()
p.updatePasswordErrorTexture()
}
} }
} }
} }
// getActiveSSID queries NetworkManager for the currently active WiFi SSID // getAiveSSID queries NetworkManager for the currently active WiFi SSID
func (p *WiFiSetupPage) getActiveSSID() string { func (p *WiFiSetupPage) getActiveSSID() string {
if p.networkManager == nil || p.wifiDevice == nil { if p.networkManager == nil || p.wifiDevice == nil {
return "" return ""
} }
nmObj := p.networkManager.Conn().Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") conn := p.networkManager.Conn()
prop, err := nmObj.GetProperty("org.freedesktop.NetworkManager.ActiveConnections")
devObj := conn.Object("org.freedesktop.NetworkManager", p.wifiDevice.Path)
activeApProp, err := devObj.GetProperty("org.freedesktop.NetworkManager.Device.Wireless.ActiveAccessPoint")
if err != nil { if err != nil {
return "" return ""
} }
if paths, ok := prop.Value().([]dbus.ObjectPath); ok { activeApPath, ok := activeApProp.Value().(dbus.ObjectPath)
for _, path := range paths { if !ok || activeApPath == "/" || activeApPath == "" {
acObj := p.networkManager.Conn().Object("org.freedesktop.NetworkManager", path) return ""
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 ""
apObj := conn.Object("org.freedesktop.NetworkManager", activeApPath)
ssidProp, err := apObj.GetProperty("org.freedesktop.NetworkManager.AccessPoint.Ssid")
if err != nil {
return ""
}
ssidBytes, ok := ssidProp.Value().([]byte)
if !ok {
return ""
}
logger.Info("Active SSID: %s", string(ssidBytes))
return string(ssidBytes)
} }
// refreshNetworkColors updates the colors of network items based on active status // refreshNetworkColors updates the colors of network items based on active status
+33 -12
View File
@@ -4,6 +4,7 @@ import (
"regexp" "regexp"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
"rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/logger" "rhemawavetech.com/rhemawave/internal/logger"
"rhemawavetech.com/rhemawave/internal/netreq" "rhemawavetech.com/rhemawave/internal/netreq"
@@ -32,6 +33,7 @@ func SyncProclamations(database *gorm.DB) ([]db.Proclamation, error) {
var storedProclamationSettings []db.Proclamation var storedProclamationSettings []db.Proclamation
database.Select("ID", "Selected").Find(&storedProclamationSettings) database.Select("ID", "Selected").Find(&storedProclamationSettings)
// [ID]Selected // [ID]Selected
stash := make(map[string]bool) stash := make(map[string]bool)
for _, row := range storedProclamationSettings { for _, row := range storedProclamationSettings {
@@ -43,20 +45,39 @@ func SyncProclamations(database *gorm.DB) ([]db.Proclamation, error) {
} }
err = database.Transaction(func(tx *gorm.DB) error { err = database.Transaction(func(tx *gorm.DB) error {
// Clear the database ids := make([]string, len(proclamations))
// Where "1 = 1" because batch Delete() requires a condition for i, p := range proclamations {
// 'Where "1 = 1"' is like saying "while true be true" and tricks GORM into deleting everything ids[i] = p.ID
if err := tx.Where("1 = 1").Delete(db.Proclamation{}).Error; err != nil { }
if len(ids) == 0 {
// API returned nothing → DB should be empty
if err := tx.Where("1 = 1").Delete(&db.Proclamation{}).Error; err != nil {
return err
}
} else {
// Remove anything not in API
if err := tx.Where("id NOT IN ?", ids).Delete(&db.Proclamation{}).Error; err != nil {
return err
}
}
// Upsert (preserve Selected automatically)
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{
"name",
"content",
"solfeggio_frequency_id",
"category_id",
"available",
"price",
}),
}).Create(&proclamations).Error; err != nil {
return err return err
} }
// Then re-populate the database
if err := tx.Save(&proclamations).Error; err != nil { // Return nil as err for this statement, does not exit the function
return err
}
// Old sync code
// if err := tx.Omit("selected").Save(&proclamations).Error; err != nil {
// return err
// }
return nil return nil
}) })
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

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