package page import ( "fmt" "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/logger" "rhemawavetech.com/rhemawave/internal/shaderManager" ) type LogViewerPage struct { Base titleTextureID uint32 titleTextureWidth int32 titleTextureHeight int32 lines []string lineTextureIDs []uint32 lineTextureWidths []int32 lineTextureHeights []int32 lineColors []sdl.Color hintTextureID uint32 hintTextureWidth int32 hintTextureHeight int32 scrollOffset int32 maxScrollOffset int32 lineHeight int32 visibleHeight int32 } func (p *LogViewerPage) Init(ctx *PageCtx) error { var logs []db.Log if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } if result := p.Context.DbPtr.Order("timestamp desc").Limit(250).Find(&logs); result.Error != nil { logger.Error("error querying database for logs: %s", result.Error) return fmt.Errorf("%s", result.Error) } // // 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")...) // } levelColors := map[string]sdl.Color{ "DEBUG": colors.SkyBlue, "INFO": colors.White, "WARNING": colors.Yellow, "ERROR": colors.RedOrange, } fallbackColor := colors.White p.lineColors = make([]sdl.Color, len(p.lines)) for _, entry := range logs { color, ok := levelColors[entry.Level] if !ok { color = fallbackColor } logTime := time.UnixMilli(entry.Timestamp) timeStr := logTime.Format("2006-01-02 15:04") line := fmt.Sprintf("[%s] [%s] %s", timeStr, entry.Level, entry.Message) p.lines = append(p.lines, line) p.lineColors = append(p.lineColors, color) } applicationFont := "Roboto-Regular" 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.Context.NavigateTo(SETTINGS) 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 { baseFontSize := p.Context.CfgPtr.StandardFontSize sm := shaderManager.Get() textShader, _ := sm.Get("text") 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}, colors.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}, colors.Gray) // 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 color := colors.White if i < len(p.lineColors) { color = p.lineColors[i] } p.Base.RenderTexture(textShader, p.lineTextureIDs[i], mgl32.Vec2{float32(contentLeftX), float32(currentY)}, mgl32.Vec2{lineW, lineH}, color) 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() }