//nolint:mnd package fbdraw import ( "os/exec" "strings" "time" ) // HeaderWrapper wraps a FrameGenerator and adds a 3-line header type HeaderWrapper struct { wrapped FrameGenerator width int height int unameCache string lsbCache string } // NewHeaderWrapper creates a new header wrapper around a FrameGenerator func NewHeaderWrapper(wrapped FrameGenerator) *HeaderWrapper { return &HeaderWrapper{ wrapped: wrapped, } } // Init initializes both the wrapper and the wrapped generator func (h *HeaderWrapper) Init(width, height int) error { h.width = width h.height = height // Cache uname output since it doesn't change // Get OS, hostname, kernel version, and architecture separately var parts []string // OS name (e.g., "Linux", "Darwin") if output, err := exec.Command("uname", "-s").Output(); err == nil { parts = append(parts, strings.TrimSpace(string(output))) } // Hostname if output, err := exec.Command("uname", "-n").Output(); err == nil { parts = append(parts, strings.TrimSpace(string(output))) } // Kernel version if output, err := exec.Command("uname", "-r").Output(); err == nil { parts = append(parts, strings.TrimSpace(string(output))) } // Machine architecture if output, err := exec.Command("uname", "-m").Output(); err == nil { parts = append(parts, strings.TrimSpace(string(output))) } if len(parts) > 0 { h.unameCache = strings.Join(parts, " ") } else { h.unameCache = "Unknown System" } // Get LSB release info if output, err := exec.Command("lsb_release", "-ds").Output(); err == nil { h.lsbCache = strings.TrimSpace(string(output)) // Remove quotes if present h.lsbCache = strings.Trim(h.lsbCache, "\"") } // Initialize wrapped generator with reduced height (minus 3 for header) return h.wrapped.Init(width, height-3) } // GenerateFrame generates a frame with header and wrapped content func (h *HeaderWrapper) GenerateFrame(grid *CharGrid) error { // Create a temporary grid for the wrapped content contentGrid := NewCharGrid(h.width, h.height-3) // Copy font settings from main grid contentGrid.FontSize = grid.FontSize contentGrid.FontFamily = grid.FontFamily // Let the wrapped generator fill its content if err := h.wrapped.GenerateFrame(contentGrid); err != nil { return err } // Now we'll assemble the final grid // First, clear the entire grid grid.Clear(Black) // Draw the header h.drawHeader(grid) // Copy content from wrapped generator below the header for y := 0; y < contentGrid.Height; y++ { for x := 0; x < contentGrid.Width; x++ { if y < len(contentGrid.Cells) && x < len(contentGrid.Cells[y]) { grid.Cells[y+3][x] = contentGrid.Cells[y][x] } } } return nil } // drawHeader draws the 3-line header func (h *HeaderWrapper) drawHeader(grid *CharGrid) { writer := NewGridWriter(grid) // Line 1: uname + lsb_release output (truncated if needed) writer.MoveAbs(0, 0) writer.SetColor(Gray60) sysInfo := h.unameCache if h.lsbCache != "" { sysInfo += " " + h.lsbCache } // Account for the UTC time on the right - never truncate time now := time.Now() utcTime := now.UTC().Format("Mon 2006-01-02 15:04:05 UTC") maxLen := h.width - len(utcTime) - 1 if len(sysInfo) > maxLen { sysInfo = sysInfo[:maxLen-3] + "..." } writer.Write("%s", sysInfo) // Check if local time is different from UTC localZone, offset := now.Zone() showLocalTime := offset != 0 // Only show local time if not UTC // Line 2: uptime output writer.MoveAbs(0, 1) if output, err := exec.Command("uptime").Output(); err == nil { uptime := strings.TrimSpace(string(output)) // Don't cut off at "user" - show the full uptime output maxLen := h.width - 1 if showLocalTime { // Account for the local time on the right - never truncate time localTime := now.Format("Mon 2006-01-02 15:04:05 ") + localZone maxLen = h.width - len(localTime) - 1 } if len(uptime) > maxLen { uptime = uptime[:maxLen-3] + "..." } writer.Write("%s", uptime) } // Right side - UTC time (line 1) - always show full time writer.MoveAbs(h.width-len(utcTime), 0) writer.Write("%s", utcTime) // Right side - Local time (line 2) - only show if different from UTC if showLocalTime { localTime := now.Format("Mon 2006-01-02 15:04:05 ") + localZone writer.MoveAbs(h.width-len(localTime), 1) writer.Write("%s", localTime) } // Line 3: Horizontal rule writer.MoveAbs(0, 2) writer.SetColor(Gray30) for i := 0; i < h.width; i++ { writer.Write("─") } } // FramesPerSecond returns the wrapped generator's frame rate func (h *HeaderWrapper) FramesPerSecond() float64 { return h.wrapped.FramesPerSecond() }