mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-19 21:00:52 +08:00
199 lines
4.5 KiB
Go
199 lines
4.5 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/viewport"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
// logsTabModel displays real-time log lines from the logrus hook.
|
|
type logsTabModel struct {
|
|
hook *LogHook
|
|
viewport viewport.Model
|
|
lines []string
|
|
maxLines int
|
|
autoScroll bool
|
|
width int
|
|
height int
|
|
ready bool
|
|
filter string // "", "debug", "info", "warn", "error"
|
|
}
|
|
|
|
// logLineMsg carries a new log line from the logrus hook channel.
|
|
type logLineMsg string
|
|
|
|
func newLogsTabModel(hook *LogHook) logsTabModel {
|
|
return logsTabModel{
|
|
hook: hook,
|
|
maxLines: 5000,
|
|
autoScroll: true,
|
|
}
|
|
}
|
|
|
|
func (m logsTabModel) Init() tea.Cmd {
|
|
return m.waitForLog
|
|
}
|
|
|
|
// waitForLog listens on the hook channel and returns a logLineMsg.
|
|
func (m logsTabModel) waitForLog() tea.Msg {
|
|
line, ok := <-m.hook.Chan()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return logLineMsg(line)
|
|
}
|
|
|
|
func (m logsTabModel) Update(msg tea.Msg) (logsTabModel, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case localeChangedMsg:
|
|
m.viewport.SetContent(m.renderLogs())
|
|
return m, nil
|
|
case logLineMsg:
|
|
m.lines = append(m.lines, string(msg))
|
|
if len(m.lines) > m.maxLines {
|
|
m.lines = m.lines[len(m.lines)-m.maxLines:]
|
|
}
|
|
m.viewport.SetContent(m.renderLogs())
|
|
if m.autoScroll {
|
|
m.viewport.GotoBottom()
|
|
}
|
|
return m, m.waitForLog
|
|
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "a":
|
|
m.autoScroll = !m.autoScroll
|
|
if m.autoScroll {
|
|
m.viewport.GotoBottom()
|
|
}
|
|
return m, nil
|
|
case "c":
|
|
m.lines = nil
|
|
m.viewport.SetContent(m.renderLogs())
|
|
return m, nil
|
|
case "1":
|
|
m.filter = ""
|
|
m.viewport.SetContent(m.renderLogs())
|
|
return m, nil
|
|
case "2":
|
|
m.filter = "info"
|
|
m.viewport.SetContent(m.renderLogs())
|
|
return m, nil
|
|
case "3":
|
|
m.filter = "warn"
|
|
m.viewport.SetContent(m.renderLogs())
|
|
return m, nil
|
|
case "4":
|
|
m.filter = "error"
|
|
m.viewport.SetContent(m.renderLogs())
|
|
return m, nil
|
|
default:
|
|
wasAtBottom := m.viewport.AtBottom()
|
|
var cmd tea.Cmd
|
|
m.viewport, cmd = m.viewport.Update(msg)
|
|
// If user scrolls up, disable auto-scroll
|
|
if !m.viewport.AtBottom() && wasAtBottom {
|
|
m.autoScroll = false
|
|
}
|
|
// If user scrolls to bottom, re-enable auto-scroll
|
|
if m.viewport.AtBottom() {
|
|
m.autoScroll = true
|
|
}
|
|
return m, cmd
|
|
}
|
|
}
|
|
|
|
var cmd tea.Cmd
|
|
m.viewport, cmd = m.viewport.Update(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
func (m *logsTabModel) SetSize(w, h int) {
|
|
m.width = w
|
|
m.height = h
|
|
if !m.ready {
|
|
m.viewport = viewport.New(w, h)
|
|
m.viewport.SetContent(m.renderLogs())
|
|
m.ready = true
|
|
} else {
|
|
m.viewport.Width = w
|
|
m.viewport.Height = h
|
|
}
|
|
}
|
|
|
|
func (m logsTabModel) View() string {
|
|
if !m.ready {
|
|
return T("loading")
|
|
}
|
|
return m.viewport.View()
|
|
}
|
|
|
|
func (m logsTabModel) renderLogs() string {
|
|
var sb strings.Builder
|
|
|
|
scrollStatus := successStyle.Render(T("logs_auto_scroll"))
|
|
if !m.autoScroll {
|
|
scrollStatus = warningStyle.Render(T("logs_paused"))
|
|
}
|
|
filterLabel := "ALL"
|
|
if m.filter != "" {
|
|
filterLabel = strings.ToUpper(m.filter) + "+"
|
|
}
|
|
|
|
header := fmt.Sprintf(" %s %s %s: %s %s: %d",
|
|
T("logs_title"), scrollStatus, T("logs_filter"), filterLabel, T("logs_lines"), len(m.lines))
|
|
sb.WriteString(titleStyle.Render(header))
|
|
sb.WriteString("\n")
|
|
sb.WriteString(helpStyle.Render(T("logs_help")))
|
|
sb.WriteString("\n")
|
|
sb.WriteString(strings.Repeat("─", m.width))
|
|
sb.WriteString("\n")
|
|
|
|
if len(m.lines) == 0 {
|
|
sb.WriteString(subtitleStyle.Render(T("logs_waiting")))
|
|
return sb.String()
|
|
}
|
|
|
|
for _, line := range m.lines {
|
|
if m.filter != "" && !m.matchLevel(line) {
|
|
continue
|
|
}
|
|
styled := m.styleLine(line)
|
|
sb.WriteString(styled)
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
func (m logsTabModel) matchLevel(line string) bool {
|
|
switch m.filter {
|
|
case "error":
|
|
return strings.Contains(line, "[error]") || strings.Contains(line, "[fatal]") || strings.Contains(line, "[panic]")
|
|
case "warn":
|
|
return strings.Contains(line, "[warn") || strings.Contains(line, "[error]") || strings.Contains(line, "[fatal]")
|
|
case "info":
|
|
return !strings.Contains(line, "[debug]")
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (m logsTabModel) styleLine(line string) string {
|
|
if strings.Contains(line, "[error]") || strings.Contains(line, "[fatal]") {
|
|
return logErrorStyle.Render(line)
|
|
}
|
|
if strings.Contains(line, "[warn") {
|
|
return logWarnStyle.Render(line)
|
|
}
|
|
if strings.Contains(line, "[info") {
|
|
return logInfoStyle.Render(line)
|
|
}
|
|
if strings.Contains(line, "[debug]") {
|
|
return logDebugStyle.Render(line)
|
|
}
|
|
return line
|
|
}
|