Update Database

Database is more usable now.
All pages have been updated to use the database, and share an application configuration.
Page Stack has been removed, no more pages hanging around in memory. Replaced with a 'Router' which uses an Enum to determine which page should be displayed, destroys the old page and initializes the new page all in one go. No more push, pop, rollback, etc. No more holding instances of pages.
Logger has been updated with proper timestamps and sorting now works as expected."
This commit is contained in:
NeilClack
2026-03-23 15:02:07 -05:00
parent f8fbfb721d
commit 17c088cc2f
28 changed files with 537 additions and 2276 deletions
+413
View File
@@ -0,0 +1,413 @@
package page
import (
"fmt"
"os"
"path/filepath"
"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/fontManager"
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
var GitVersion = "dev"
type Home 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 *Home) updateDisplayCounts() error {
var err 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
baseFontSize := p.Base.Context.CfgPtr.StandardFontSize
// 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 *Home) Init(ctx *PageCtx) 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(ctx); 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)
}
// Create license info textures if data exists
if p.Base.Context.CfgPtr.EmailAddress != "" || p.Base.Context.CfgPtr.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 p.Base.Context.CfgPtr.EmailAddress != "" {
emailText := fmt.Sprintf("Email: %s", p.Base.Context.CfgPtr.EmailAddress)
p.emailDisplay, err = NewStringItem(emailText, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create email display texture: %w", err)
}
}
// Create license key display if exists
if p.Base.Context.CfgPtr.LicenseKey != "" {
licenseKeyText := fmt.Sprintf("License Key: %s", p.Base.Context.CfgPtr.LicenseKey)
p.licenseKeyDisplay, err = NewStringItem(licenseKeyText, font, colors.White)
if err != nil {
return fmt.Errorf("unable to create license key display texture: %w", err)
}
}
}
// Get base font size from database
baseFontSize := p.Base.Context.CfgPtr.StandardFontSize
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 Home page.
func (p *Home) 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 *Home) 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 *Home) 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 *Home) 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()
}