package tui import ( "errors" "fmt" "io" "os" "strings" tea "charm.land/bubbletea/v2" "gitgud.io/mike/mpv-manager/pkg/log" ) const ( // maxLogTailBytes caps how much is read from the end of the log file. maxLogTailBytes int64 = 256 << 10 // maxLogTailLines caps how many trailing lines are shown in the viewer. maxLogTailLines = 500 ) // readLogTail returns up to maxLines trailing lines of the file at path, // reading at most maxBytes from the end. truncated reports whether any // content was cut. func readLogTail(path string, maxBytes int64, maxLines int) (content string, truncated bool, err error) { file, err := os.Open(path) if err != nil { return "", false, err } defer file.Close() info, err := file.Stat() if err != nil { return "", false, err } if info.Size() > maxBytes { truncated = true if _, err := file.Seek(info.Size()-maxBytes, io.SeekStart); err != nil { return "", false, err } } data, err := io.ReadAll(io.LimitReader(file, maxBytes)) if err != nil { return "", false, err } text := string(data) if truncated { // Drop the first, potentially partial, line. if idx := strings.IndexByte(text, '\n'); idx >= 0 { text = text[idx+1:] } } lines := strings.Split(strings.TrimRight(text, "\n"), "\n") if len(lines) > maxLines { lines = lines[len(lines)-maxLines:] truncated = true } return strings.Join(lines, "\n"), truncated, nil } // setupLogsView loads the tail of the mpv-manager log file into the viewport. func (m *Model) setupLogsView() { m.logsStatusMsg = "" logPath := log.GetFilePath() if logPath == "" { m.logsStatusMsg = "Log file is not available (logger not initialized)." m.logsViewport.SetContent("") return } content, truncated, err := readLogTail(logPath, maxLogTailBytes, maxLogTailLines) if err != nil { if errors.Is(err, os.ErrNotExist) { m.logsStatusMsg = "No log file found at " + logPath } else { m.logsStatusMsg = "Failed to read log file: " + err.Error() } m.logsViewport.SetContent("") return } switch { case strings.TrimSpace(content) == "": m.logsStatusMsg = "Log file is empty: " + logPath case truncated: m.logsStatusMsg = fmt.Sprintf("Showing last %d lines of %s", maxLogTailLines, logPath) default: m.logsStatusMsg = logPath } m.logsViewport.SetContent(sanitizeTerminalText(content)) m.logsViewport.GotoBottom() } // handleLogsViewerUpdate handles the logs viewer scroll state. func (m *Model) handleLogsViewerUpdate(msg tea.Msg) (*Model, tea.Cmd) { if keyMsg, ok := msg.(tea.KeyPressMsg); ok { switch keyCode(keyMsg) { case tea.KeyEsc: m.resetToMainMenu() return m, nil case tea.KeyExtended: if runes := keyTextRunes(keyMsg); len(runes) == 1 && (runes[0] == 'r' || runes[0] == 'R') { m.setupLogsView() return m, nil } } } var cmd tea.Cmd m.logsViewport, cmd = m.logsViewport.Update(msg) return m, cmd } // logsViewerView renders the log tail viewer. func (m Model) logsViewerView() string { var content strings.Builder content.WriteString(titleStyle.Render("📄 Logs")) content.WriteString("\n") content.WriteString(subtitleStyle.Render("═══════")) content.WriteString("\n\n") if m.logsStatusMsg != "" { content.WriteString(infoStyle.Render(m.logsStatusMsg)) content.WriteString("\n\n") } content.WriteString(m.logsViewport.View()) content.WriteString("\n") content.WriteString(infoStyle.Render("Press ")) content.WriteString(subtitleStyle.Render("↑/↓")) content.WriteString(infoStyle.Render(" to scroll, ")) content.WriteString(subtitleStyle.Render("R")) content.WriteString(infoStyle.Render(" to reload, ")) content.WriteString(subtitleStyle.Render("Esc")) content.WriteString(infoStyle.Render(" to go back")) return content.String() }