diff --git a/internal/tui/model_test.go b/internal/tui/model_test.go index 8b4de80..230e399 100644 --- a/internal/tui/model_test.go +++ b/internal/tui/model_test.go @@ -28,22 +28,25 @@ func (f *fakeModelRuntime) startResolve(state *auth.State, ch chan<- StreamerUpd func TestViewDisplaysDashboardWithSelectedStreamerDetails(t *testing.T) { model := dashboardModel( - twitch.StreamerEntry{ConfigLogin: "alpha", Login: "alpha_live", Live: true, Status: twitch.StreamerReady}, + twitch.StreamerEntry{ConfigLogin: "alpha", Login: "alpha_live", ChannelID: "1", Live: true, Status: twitch.StreamerReady}, twitch.StreamerEntry{ConfigLogin: "beta", Login: "beta_live", Live: true, Status: twitch.StreamerReady}, twitch.StreamerEntry{ConfigLogin: "gamma", Status: twitch.StreamerLoading}, ) - assertContainsAll(t, model.View(), + view := model.View() + assertContainsAll(t, view, "Watching: alpha_live, beta_live", "Info", - "IRC", + "Chat", "Miner", - "live | irc idle", "gamma", - "loading", + "alpha_live ●", + "Channel: alpha_live", + "Channel ID: 1", "Status: live", - "IRC: not joined", + "Chat: not joined", ) + assertInOrder(t, view, "Channel: alpha_live", "Channel ID: 1", "Status: live", "Chat: not joined") } func TestAuthUpdateAppendsLogLine(t *testing.T) { @@ -125,6 +128,19 @@ func TestViewDisplaysInactiveDetailForOfflineStreamer(t *testing.T) { assertContainsAll(t, model.View(), "offline", "inactive") } +func assertInOrder(t *testing.T, value string, parts ...string) { + t.Helper() + + offset := 0 + for _, part := range parts { + index := strings.Index(value[offset:], part) + if index < 0 { + t.Fatalf("expected %q after offset %d in:\n%s", part, offset, value) + } + offset += index + len(part) + } +} + func TestActiveStreamersRenderBeforeInactiveInConfigOrder(t *testing.T) { model := New(Options{initialStreamers: []twitch.StreamerEntry{ {ConfigLogin: "alpha", Login: "alpha_live", Status: twitch.StreamerReady}, diff --git a/internal/tui/styles.go b/internal/tui/styles.go index aafa365..3b437b3 100644 --- a/internal/tui/styles.go +++ b/internal/tui/styles.go @@ -5,55 +5,53 @@ import ( "github.com/charmbracelet/lipgloss" ) +var ( + titleColor = lipgloss.Color("#E6EDF3") + mutedColor = lipgloss.Color("#8B949E") + accentColor = lipgloss.Color("#7CE38B") + warnColor = lipgloss.Color("#F0883E") + errorColor = lipgloss.Color("#FF7B72") + panelBorderColor = lipgloss.Color("#30363D") + tabActiveTextColor = lipgloss.Color("#0D1117") + tabInactiveBgColor = lipgloss.Color("#1B1F24") + rowTextColor = lipgloss.Color("#C9D1D9") +) + var ( pageStyle = lipgloss.NewStyle(). Padding(1, 2) titleStyle = lipgloss.NewStyle(). Bold(true). - Foreground(lipgloss.Color("#E6EDF3")) + Foreground(titleColor) mutedStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#6E7681")) + Foreground(mutedColor) accentStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#7CE38B")) + Foreground(accentColor) warnStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#F0883E")) + Foreground(warnColor) errorStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FF7B72")) + Foreground(errorColor) panelStyle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("#30363D")). + BorderForeground(panelBorderColor). Padding(1, 2) - focusedPanelStyle = panelStyle. - BorderForeground(lipgloss.Color("#7CE38B")) - labelStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#8B949E")). + focusedPanelStyle = panelStyle.Copy(). + BorderForeground(accentColor) + labelStyle = mutedStyle.Copy(). Bold(true) - activeTabStyle = lipgloss.NewStyle(). + activeTabStyle = accentStyle.Copy(). Bold(true). - Foreground(lipgloss.Color("#0D1117")). - Background(lipgloss.Color("#7CE38B")) - inactiveTabStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#8B949E")). - Background(lipgloss.Color("#1B1F24")) - statusLiveStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#7CE38B")) - statusIdleStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#8B949E")) - statusErrorStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FF7B72")) + Foreground(tabActiveTextColor). + Background(accentColor) + inactiveTabStyle = mutedStyle.Copy(). + Background(tabInactiveBgColor) rowNameStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#C9D1D9")). + Foreground(rowTextColor). PaddingLeft(1) - rowDescStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#8B949E")). - PaddingLeft(1) - selectedRowNameStyle = rowNameStyle. + selectedRowNameStyle = rowNameStyle.Copy(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(lipgloss.Color("#7CE38B")). - Foreground(lipgloss.Color("#E6EDF3")) - selectedRowDescStyle = rowDescStyle. - Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(lipgloss.Color("#7CE38B")) + BorderForeground(accentColor). + Foreground(titleColor) ) func newAuthViewport(width, height int) viewport.Model { diff --git a/internal/tui/view.go b/internal/tui/view.go index de700c9..7a04e37 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -96,7 +96,7 @@ func (m Model) renderDetailPanel() string { func (m Model) renderDetailTabs() string { tabs := []string{ m.renderDetailTabButton(infoTab, "Info"), - m.renderDetailTabButton(ircTab, "IRC"), + m.renderDetailTabButton(ircTab, "Chat"), m.renderDetailTabButton(minerTab, "Miner"), } return lipgloss.JoinHorizontal(lipgloss.Top, tabs...) @@ -111,22 +111,23 @@ func (m Model) renderDetailTabButton(tab detailTab, label string) string { func (m Model) renderInfoTab(entry twitch.StreamerEntry) string { lines := []string{ - "Status: " + statusText(entry), + "Channel: " + streamerName(entry), } if entry.ChannelID != "" { lines = append(lines, "Channel ID: "+entry.ChannelID) } + lines = append(lines, "Status: "+statusText(entry)) detail := m.ircDetails[normalizeKey(entry.Login)] if !isActive(entry) { - lines = append(lines, "IRC: "+mutedStyle.Render("inactive")) + lines = append(lines, "Chat: "+mutedStyle.Render("inactive")) return strings.Join(lines, "\n") } if detail.joined { - lines = append(lines, "IRC: "+accentStyle.Render("joined")) + lines = append(lines, "Chat: "+accentStyle.Render("joined")) } else { - lines = append(lines, "IRC: "+mutedStyle.Render("not joined")) + lines = append(lines, "Chat: "+mutedStyle.Render("not joined")) } return strings.Join(lines, "\n") } @@ -145,24 +146,24 @@ func (m Model) renderStreamerRows() string { return mutedStyle.Render("none") } - lines := make([]string, 0, len(entries)*2) + lines := make([]string, 0, len(entries)) for _, entry := range entries { - nameStyle, descStyle := rowNameStyle, rowDescStyle + nameStyle := rowNameStyle if entry.ConfigLogin == m.selectedConfig { - nameStyle, descStyle = selectedRowNameStyle, selectedRowDescStyle + nameStyle = selectedRowNameStyle } - detail := m.ircDetails[normalizeKey(entry.Login)] - lines = append(lines, - nameStyle.Render(streamerName(entry)), - descStyle.Render(rawStatus(entry)+" | "+ircSummary(detail)), - ) + label := nameStyle.Render(streamerName(entry)) + if isActive(entry) { + label += " " + accentStyle.Render("●") + } + lines = append(lines, label) } return strings.Join(lines, "\n") } func (m Model) visibleStreamers() []twitch.StreamerEntry { entries := m.orderedStreamers() - visible := max(1, streamerListHeight(m.height)/2) + visible := max(1, streamerListHeight(m.height)) if len(entries) <= visible { return entries } @@ -202,13 +203,13 @@ func watchingSummary(streamers []twitch.StreamerEntry) string { func statusText(entry twitch.StreamerEntry) string { switch { case entry.Status == twitch.StreamerError: - return statusErrorStyle.Render("error") + return errorStyle.Render("error") case entry.Status == twitch.StreamerLoading: return mutedStyle.Render("loading") case entry.Live: - return statusLiveStyle.Render("live") + return accentStyle.Render("live") default: - return statusIdleStyle.Render("offline") + return mutedStyle.Render("offline") } }