feat: add miner event log tab

This commit is contained in:
2026-04-29 23:38:38 +02:00
parent 61e140fb03
commit f131f0bc71
9 changed files with 371 additions and 36 deletions
+89 -17
View File
@@ -26,7 +26,8 @@ type panelFocus int
const (
focusStreamers panelFocus = iota
focusInfo
focusChat
focusIRC
focusMiner
)
type detailTab int
@@ -34,6 +35,7 @@ type detailTab int
const (
infoTab detailTab = iota
ircTab
minerTab
)
type modelRuntime interface {
@@ -58,8 +60,10 @@ type Model struct {
selectedConfig string
focus panelFocus
ircDetails map[string]ircDetail
minerDetails map[string][]string
authViewport viewport.Model
ircViewport viewport.Model
minerViewport viewport.Model
}
type ircDetail struct {
@@ -81,18 +85,23 @@ func New(options Options) Model {
authLogs: []string{},
mode: authView,
ircDetails: make(map[string]ircDetail),
minerDetails: make(map[string][]string),
width: defaultViewWidth,
height: 24,
focus: focusStreamers,
authViewport: newAuthViewport(contentWidth(defaultViewWidth), authViewportHeight(24)),
ircViewport: newIRCViewport(detailViewportWidth(defaultViewWidth), detailViewportHeight(24)),
minerViewport: newMinerViewport(
detailViewportWidth(defaultViewWidth),
detailViewportHeight(24),
),
}
if options.AuthState != nil {
model.mode = streamerView
}
model.ensureSelection()
model.syncAuthViewport()
model.syncIRCViewport(true)
model.syncDetailViewports(true)
return model
}
@@ -122,7 +131,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height
m.resizeComponents()
m.ensureSelection()
m.syncIRCViewport(false)
m.syncDetailViewports(false)
return m, nil
case tea.KeyMsg:
switch msg.String() {
@@ -162,11 +171,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.authViewport, cmd = m.authViewport.Update(msg)
return m, cmd
}
if m.focus == focusChat {
if m.focus == focusIRC {
var cmd tea.Cmd
m.ircViewport, cmd = m.ircViewport.Update(msg)
return m, cmd
}
if m.focus == focusMiner {
var cmd tea.Cmd
m.minerViewport, cmd = m.minerViewport.Update(msg)
return m, cmd
}
return m, nil
}
@@ -189,11 +203,12 @@ func (m Model) updateAuth(msg AuthUpdate) (tea.Model, tea.Cmd) {
m.resolveErr = nil
m.streamers = loadingEntries(m.streamers)
m.ircDetails = make(map[string]ircDetail)
m.minerDetails = make(map[string][]string)
m.selectedConfig = ""
m.focus = focusStreamers
m.ensureSelection()
m.resizeComponents()
m.syncIRCViewport(true)
m.syncDetailViewports(true)
return m, startStreamerResolution(m.runtime, m.authState)
}
return m, nil
@@ -215,12 +230,15 @@ func (m Model) updateStreamer(msg StreamerUpdate) (tea.Model, tea.Cmd) {
if msg.IRC != nil {
m.applyIRCUpdate(*msg.IRC)
}
if msg.Miner != nil {
m.applyMinerUpdate(*msg.Miner)
}
if msg.Err != nil {
m.resolveErr = msg.Err
}
m.ensureSelection()
m.resizeComponents()
m.syncIRCViewport(false)
m.syncDetailViewports(false)
if msg.Done {
return m, nil
}
@@ -244,14 +262,20 @@ func (m *Model) applyIRCUpdate(update IRCUpdate) {
detail.joined = false
}
if message, ok := formatIRCChatLine(update.Line); ok {
detail.messages = append(detail.messages, message)
if len(detail.messages) > maxIRCMessageHistory {
detail.messages = append([]string(nil), detail.messages[len(detail.messages)-maxIRCMessageHistory:]...)
}
detail.messages = appendCappedHistory(detail.messages, message)
}
m.ircDetails[login] = detail
}
func (m *Model) applyMinerUpdate(update MinerUpdate) {
login := normalizeKey(update.Login)
line := strings.TrimSpace(update.Line)
if login == "" || line == "" {
return
}
m.minerDetails[login] = appendCappedHistory(m.minerDetails[login], line)
}
func (m *Model) ensureSelection() {
entries := m.orderedStreamers()
if len(entries) == 0 {
@@ -274,7 +298,7 @@ func (m *Model) moveSelection(delta int) {
entries := m.orderedStreamers()
if len(entries) == 0 {
m.selectedConfig = ""
m.syncIRCViewport(true)
m.syncDetailViewports(true)
return
}
@@ -288,7 +312,7 @@ func (m *Model) moveSelection(delta int) {
}
m.selectedConfig = entries[selected].ConfigLogin
if m.selectedConfig != current {
m.syncIRCViewport(true)
m.syncDetailViewports(true)
}
}
@@ -296,8 +320,10 @@ func (m *Model) moveFocusLeft() {
switch m.focus {
case focusInfo:
m.focus = focusStreamers
case focusChat:
case focusIRC:
m.focus = focusInfo
case focusMiner:
m.focus = focusIRC
}
}
@@ -306,7 +332,9 @@ func (m *Model) moveFocusRight() {
case focusStreamers:
m.focus = focusInfo
case focusInfo:
m.focus = focusChat
m.focus = focusIRC
case focusIRC:
m.focus = focusMiner
}
}
@@ -343,6 +371,8 @@ func (m *Model) resizeComponents() {
m.authViewport.Height = authViewportHeight(m.height)
m.ircViewport.Width = detailViewportWidth(m.width)
m.ircViewport.Height = detailViewportHeight(m.height)
m.minerViewport.Width = detailViewportWidth(m.width)
m.minerViewport.Height = detailViewportHeight(m.height)
}
func (m *Model) syncAuthViewport() {
@@ -369,6 +399,23 @@ func (m *Model) syncIRCViewport(forceBottom bool) {
}
}
func (m *Model) syncMinerViewport(forceBottom bool) {
if m.minerViewport.Width <= 0 || m.minerViewport.Height <= 0 {
return
}
stickToBottom := forceBottom || m.minerViewport.AtBottom()
m.minerViewport.SetContent(m.minerViewportContent())
if stickToBottom {
m.minerViewport.GotoBottom()
}
}
func (m *Model) syncDetailViewports(forceBottom bool) {
m.syncIRCViewport(forceBottom)
m.syncMinerViewport(forceBottom)
}
func (m Model) ircViewportContent() string {
entry, ok := m.selectedEntry()
if !ok {
@@ -388,6 +435,19 @@ func (m Model) ircViewportContent() string {
return strings.Join(detail.messages, "\n")
}
func (m Model) minerViewportContent() string {
entry, ok := m.selectedEntry()
if !ok {
return "No streamers configured"
}
logins := m.minerDetails[normalizeKey(entry.Login)]
if len(logins) == 0 {
return "Waiting for miner activity..."
}
return strings.Join(logins, "\n")
}
func (m Model) selectedEntry() (twitch.StreamerEntry, bool) {
entries := m.orderedStreamers()
selected := m.selectedRowIndex(entries)
@@ -398,10 +458,14 @@ func (m Model) selectedEntry() (twitch.StreamerEntry, bool) {
}
func (m Model) visibleDetailTab() detailTab {
if m.focus == focusChat {
switch m.focus {
case focusIRC:
return ircTab
case focusMiner:
return minerTab
default:
return infoTab
}
return infoTab
}
func (m Model) isStreamersFocused() bool {
@@ -409,7 +473,7 @@ func (m Model) isStreamersFocused() bool {
}
func (m Model) isRightPanelFocused() bool {
return m.focus == focusInfo || m.focus == focusChat
return m.focus == focusInfo || m.focus == focusIRC || m.focus == focusMiner
}
func isActive(entry twitch.StreamerEntry) bool {
@@ -450,6 +514,14 @@ func formatIRCChatLine(line string) (string, bool) {
return user + ": " + message, true
}
func appendCappedHistory(history []string, line string) []string {
history = append(history, line)
if len(history) <= maxIRCMessageHistory {
return history
}
return append([]string(nil), history[len(history)-maxIRCMessageHistory:]...)
}
func loadingEntries(entries []twitch.StreamerEntry) []twitch.StreamerEntry {
logins := make([]string, 0, len(entries))
for _, entry := range entries {