feat: refine tui chat and streamer panels
This commit is contained in:
@@ -28,22 +28,25 @@ func (f *fakeModelRuntime) startResolve(state *auth.State, ch chan<- StreamerUpd
|
|||||||
|
|
||||||
func TestViewDisplaysDashboardWithSelectedStreamerDetails(t *testing.T) {
|
func TestViewDisplaysDashboardWithSelectedStreamerDetails(t *testing.T) {
|
||||||
model := dashboardModel(
|
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: "beta", Login: "beta_live", Live: true, Status: twitch.StreamerReady},
|
||||||
twitch.StreamerEntry{ConfigLogin: "gamma", Status: twitch.StreamerLoading},
|
twitch.StreamerEntry{ConfigLogin: "gamma", Status: twitch.StreamerLoading},
|
||||||
)
|
)
|
||||||
|
|
||||||
assertContainsAll(t, model.View(),
|
view := model.View()
|
||||||
|
assertContainsAll(t, view,
|
||||||
"Watching: alpha_live, beta_live",
|
"Watching: alpha_live, beta_live",
|
||||||
"Info",
|
"Info",
|
||||||
"IRC",
|
"Chat",
|
||||||
"Miner",
|
"Miner",
|
||||||
"live | irc idle",
|
|
||||||
"gamma",
|
"gamma",
|
||||||
"loading",
|
"alpha_live ●",
|
||||||
|
"Channel: alpha_live",
|
||||||
|
"Channel ID: 1",
|
||||||
"Status: live",
|
"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) {
|
func TestAuthUpdateAppendsLogLine(t *testing.T) {
|
||||||
@@ -125,6 +128,19 @@ func TestViewDisplaysInactiveDetailForOfflineStreamer(t *testing.T) {
|
|||||||
assertContainsAll(t, model.View(), "offline", "inactive")
|
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) {
|
func TestActiveStreamersRenderBeforeInactiveInConfigOrder(t *testing.T) {
|
||||||
model := New(Options{initialStreamers: []twitch.StreamerEntry{
|
model := New(Options{initialStreamers: []twitch.StreamerEntry{
|
||||||
{ConfigLogin: "alpha", Login: "alpha_live", Status: twitch.StreamerReady},
|
{ConfigLogin: "alpha", Login: "alpha_live", Status: twitch.StreamerReady},
|
||||||
|
|||||||
+30
-32
@@ -5,55 +5,53 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"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 (
|
var (
|
||||||
pageStyle = lipgloss.NewStyle().
|
pageStyle = lipgloss.NewStyle().
|
||||||
Padding(1, 2)
|
Padding(1, 2)
|
||||||
titleStyle = lipgloss.NewStyle().
|
titleStyle = lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#E6EDF3"))
|
Foreground(titleColor)
|
||||||
mutedStyle = lipgloss.NewStyle().
|
mutedStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#6E7681"))
|
Foreground(mutedColor)
|
||||||
accentStyle = lipgloss.NewStyle().
|
accentStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#7CE38B"))
|
Foreground(accentColor)
|
||||||
warnStyle = lipgloss.NewStyle().
|
warnStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#F0883E"))
|
Foreground(warnColor)
|
||||||
errorStyle = lipgloss.NewStyle().
|
errorStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#FF7B72"))
|
Foreground(errorColor)
|
||||||
panelStyle = lipgloss.NewStyle().
|
panelStyle = lipgloss.NewStyle().
|
||||||
Border(lipgloss.NormalBorder()).
|
Border(lipgloss.NormalBorder()).
|
||||||
BorderForeground(lipgloss.Color("#30363D")).
|
BorderForeground(panelBorderColor).
|
||||||
Padding(1, 2)
|
Padding(1, 2)
|
||||||
focusedPanelStyle = panelStyle.
|
focusedPanelStyle = panelStyle.Copy().
|
||||||
BorderForeground(lipgloss.Color("#7CE38B"))
|
BorderForeground(accentColor)
|
||||||
labelStyle = lipgloss.NewStyle().
|
labelStyle = mutedStyle.Copy().
|
||||||
Foreground(lipgloss.Color("#8B949E")).
|
|
||||||
Bold(true)
|
Bold(true)
|
||||||
activeTabStyle = lipgloss.NewStyle().
|
activeTabStyle = accentStyle.Copy().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#0D1117")).
|
Foreground(tabActiveTextColor).
|
||||||
Background(lipgloss.Color("#7CE38B"))
|
Background(accentColor)
|
||||||
inactiveTabStyle = lipgloss.NewStyle().
|
inactiveTabStyle = mutedStyle.Copy().
|
||||||
Foreground(lipgloss.Color("#8B949E")).
|
Background(tabInactiveBgColor)
|
||||||
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"))
|
|
||||||
rowNameStyle = lipgloss.NewStyle().
|
rowNameStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#C9D1D9")).
|
Foreground(rowTextColor).
|
||||||
PaddingLeft(1)
|
PaddingLeft(1)
|
||||||
rowDescStyle = lipgloss.NewStyle().
|
selectedRowNameStyle = rowNameStyle.Copy().
|
||||||
Foreground(lipgloss.Color("#8B949E")).
|
|
||||||
PaddingLeft(1)
|
|
||||||
selectedRowNameStyle = rowNameStyle.
|
|
||||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||||
BorderForeground(lipgloss.Color("#7CE38B")).
|
BorderForeground(accentColor).
|
||||||
Foreground(lipgloss.Color("#E6EDF3"))
|
Foreground(titleColor)
|
||||||
selectedRowDescStyle = rowDescStyle.
|
|
||||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
|
||||||
BorderForeground(lipgloss.Color("#7CE38B"))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newAuthViewport(width, height int) viewport.Model {
|
func newAuthViewport(width, height int) viewport.Model {
|
||||||
|
|||||||
+18
-17
@@ -96,7 +96,7 @@ func (m Model) renderDetailPanel() string {
|
|||||||
func (m Model) renderDetailTabs() string {
|
func (m Model) renderDetailTabs() string {
|
||||||
tabs := []string{
|
tabs := []string{
|
||||||
m.renderDetailTabButton(infoTab, "Info"),
|
m.renderDetailTabButton(infoTab, "Info"),
|
||||||
m.renderDetailTabButton(ircTab, "IRC"),
|
m.renderDetailTabButton(ircTab, "Chat"),
|
||||||
m.renderDetailTabButton(minerTab, "Miner"),
|
m.renderDetailTabButton(minerTab, "Miner"),
|
||||||
}
|
}
|
||||||
return lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
|
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 {
|
func (m Model) renderInfoTab(entry twitch.StreamerEntry) string {
|
||||||
lines := []string{
|
lines := []string{
|
||||||
"Status: " + statusText(entry),
|
"Channel: " + streamerName(entry),
|
||||||
}
|
}
|
||||||
if entry.ChannelID != "" {
|
if entry.ChannelID != "" {
|
||||||
lines = append(lines, "Channel ID: "+entry.ChannelID)
|
lines = append(lines, "Channel ID: "+entry.ChannelID)
|
||||||
}
|
}
|
||||||
|
lines = append(lines, "Status: "+statusText(entry))
|
||||||
|
|
||||||
detail := m.ircDetails[normalizeKey(entry.Login)]
|
detail := m.ircDetails[normalizeKey(entry.Login)]
|
||||||
if !isActive(entry) {
|
if !isActive(entry) {
|
||||||
lines = append(lines, "IRC: "+mutedStyle.Render("inactive"))
|
lines = append(lines, "Chat: "+mutedStyle.Render("inactive"))
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if detail.joined {
|
if detail.joined {
|
||||||
lines = append(lines, "IRC: "+accentStyle.Render("joined"))
|
lines = append(lines, "Chat: "+accentStyle.Render("joined"))
|
||||||
} else {
|
} else {
|
||||||
lines = append(lines, "IRC: "+mutedStyle.Render("not joined"))
|
lines = append(lines, "Chat: "+mutedStyle.Render("not joined"))
|
||||||
}
|
}
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
@@ -145,24 +146,24 @@ func (m Model) renderStreamerRows() string {
|
|||||||
return mutedStyle.Render("none")
|
return mutedStyle.Render("none")
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := make([]string, 0, len(entries)*2)
|
lines := make([]string, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
nameStyle, descStyle := rowNameStyle, rowDescStyle
|
nameStyle := rowNameStyle
|
||||||
if entry.ConfigLogin == m.selectedConfig {
|
if entry.ConfigLogin == m.selectedConfig {
|
||||||
nameStyle, descStyle = selectedRowNameStyle, selectedRowDescStyle
|
nameStyle = selectedRowNameStyle
|
||||||
}
|
}
|
||||||
detail := m.ircDetails[normalizeKey(entry.Login)]
|
label := nameStyle.Render(streamerName(entry))
|
||||||
lines = append(lines,
|
if isActive(entry) {
|
||||||
nameStyle.Render(streamerName(entry)),
|
label += " " + accentStyle.Render("●")
|
||||||
descStyle.Render(rawStatus(entry)+" | "+ircSummary(detail)),
|
}
|
||||||
)
|
lines = append(lines, label)
|
||||||
}
|
}
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) visibleStreamers() []twitch.StreamerEntry {
|
func (m Model) visibleStreamers() []twitch.StreamerEntry {
|
||||||
entries := m.orderedStreamers()
|
entries := m.orderedStreamers()
|
||||||
visible := max(1, streamerListHeight(m.height)/2)
|
visible := max(1, streamerListHeight(m.height))
|
||||||
if len(entries) <= visible {
|
if len(entries) <= visible {
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
@@ -202,13 +203,13 @@ func watchingSummary(streamers []twitch.StreamerEntry) string {
|
|||||||
func statusText(entry twitch.StreamerEntry) string {
|
func statusText(entry twitch.StreamerEntry) string {
|
||||||
switch {
|
switch {
|
||||||
case entry.Status == twitch.StreamerError:
|
case entry.Status == twitch.StreamerError:
|
||||||
return statusErrorStyle.Render("error")
|
return errorStyle.Render("error")
|
||||||
case entry.Status == twitch.StreamerLoading:
|
case entry.Status == twitch.StreamerLoading:
|
||||||
return mutedStyle.Render("loading")
|
return mutedStyle.Render("loading")
|
||||||
case entry.Live:
|
case entry.Live:
|
||||||
return statusLiveStyle.Render("live")
|
return accentStyle.Render("live")
|
||||||
default:
|
default:
|
||||||
return statusIdleStyle.Render("offline")
|
return mutedStyle.Render("offline")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user