refactor: app orchestration into tui
This commit is contained in:
+126
-378
@@ -1,13 +1,10 @@
|
||||
// model.go contains the Bubble Tea state machine for the current terminal UI.
|
||||
// It switches between the Twitch login log view and the streamer list view,
|
||||
// and applies auth progress messages emitted by the app layer.
|
||||
package tui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"parasocial/internal/auth"
|
||||
@@ -15,7 +12,6 @@ import (
|
||||
)
|
||||
|
||||
const defaultViewWidth = 80
|
||||
const greenDot = "\x1b[32m●\x1b[0m"
|
||||
|
||||
type viewMode int
|
||||
|
||||
@@ -24,70 +20,16 @@ const (
|
||||
streamerView
|
||||
)
|
||||
|
||||
// IRCState describes the current IRC join lifecycle for one streamer row.
|
||||
type IRCState string
|
||||
|
||||
const (
|
||||
IRCPending IRCState = "pending"
|
||||
IRCJoined IRCState = "joined"
|
||||
IRCDisconnected IRCState = "disconnected"
|
||||
)
|
||||
|
||||
// StartAuthFunc begins the asynchronous Twitch login flow for the model.
|
||||
type StartAuthFunc func(chan<- AuthUpdate)
|
||||
|
||||
// StartResolveFunc begins the asynchronous viewer and streamer resolution flow for the model.
|
||||
type StartResolveFunc func(*auth.State, chan<- StreamerUpdate)
|
||||
|
||||
// AuthUpdate carries one incremental auth log line or completion result into the TUI.
|
||||
type AuthUpdate struct {
|
||||
Line string
|
||||
State *auth.State
|
||||
Err error
|
||||
Done bool
|
||||
}
|
||||
|
||||
// StreamerUpdate carries one streamer resolution update into the TUI.
|
||||
type StreamerUpdate struct {
|
||||
Viewer *twitch.Viewer
|
||||
Entry *twitch.StreamerEntry
|
||||
IRC *IRCUpdate
|
||||
Index int
|
||||
Err error
|
||||
Done bool
|
||||
}
|
||||
|
||||
// IRCUpdate carries one IRC connection state or log line into the TUI.
|
||||
type IRCUpdate struct {
|
||||
Login string
|
||||
State IRCState
|
||||
Line string
|
||||
}
|
||||
|
||||
// Options configures the initial streamer list, auth state, and worker starter hooks.
|
||||
type Options struct {
|
||||
Streamers []twitch.StreamerEntry
|
||||
AuthState *auth.State
|
||||
StartAuth StartAuthFunc
|
||||
StartResolve StartResolveFunc
|
||||
}
|
||||
|
||||
// authStartedMsg hands the model the channel that will stream auth updates.
|
||||
type authStartedMsg struct {
|
||||
Updates <-chan AuthUpdate
|
||||
}
|
||||
|
||||
// streamerStartedMsg hands the model the channel that will stream streamer updates.
|
||||
type streamerStartedMsg struct {
|
||||
Updates <-chan StreamerUpdate
|
||||
type modelRuntime interface {
|
||||
startAuth(chan<- AuthUpdate)
|
||||
startResolve(*auth.State, chan<- StreamerUpdate)
|
||||
}
|
||||
|
||||
// Model is the terminal UI for auth and streamer display.
|
||||
type Model struct {
|
||||
streamers []twitch.StreamerEntry
|
||||
authState *auth.State
|
||||
startAuth StartAuthFunc
|
||||
startResolve StartResolveFunc
|
||||
runtime modelRuntime
|
||||
authUpdates <-chan AuthUpdate
|
||||
streamerUpdates <-chan StreamerUpdate
|
||||
authLogs []string
|
||||
@@ -99,44 +41,49 @@ type Model struct {
|
||||
height int
|
||||
selectedConfig string
|
||||
ircDetails map[string]ircDetail
|
||||
authViewport viewport.Model
|
||||
}
|
||||
|
||||
type ircDetail struct {
|
||||
joined bool
|
||||
}
|
||||
|
||||
type streamerRow struct {
|
||||
index int
|
||||
entry twitch.StreamerEntry
|
||||
line string
|
||||
}
|
||||
|
||||
// New returns a Bubble Tea model for auth and streamer display.
|
||||
func New(options Options) Model {
|
||||
streamers := twitch.LoadingStreamerEntries(options.Streamers)
|
||||
if len(options.initialStreamers) > 0 {
|
||||
streamers = append([]twitch.StreamerEntry(nil), options.initialStreamers...)
|
||||
}
|
||||
|
||||
model := Model{
|
||||
streamers: append([]twitch.StreamerEntry(nil), options.Streamers...),
|
||||
streamers: streamers,
|
||||
authState: options.AuthState,
|
||||
startAuth: options.StartAuth,
|
||||
startResolve: options.StartResolve,
|
||||
runtime: options.runtime,
|
||||
authLogs: []string{},
|
||||
mode: authView,
|
||||
ircDetails: make(map[string]ircDetail),
|
||||
width: defaultViewWidth,
|
||||
height: 24,
|
||||
authViewport: newAuthViewport(contentWidth(defaultViewWidth), authViewportHeight(24)),
|
||||
}
|
||||
if options.AuthState != nil {
|
||||
model.mode = streamerView
|
||||
}
|
||||
model.ensureSelection()
|
||||
model.syncAuthViewport()
|
||||
return model
|
||||
}
|
||||
|
||||
// Init kicks off authentication only when the UI starts in the login state.
|
||||
func (m Model) Init() tea.Cmd {
|
||||
if m.mode == authView {
|
||||
return startAuthSession(m.startAuth)
|
||||
return startAuthSession(m.runtime)
|
||||
}
|
||||
return startStreamerResolution(m.startResolve, m.authState)
|
||||
return startStreamerResolution(m.runtime, m.authState)
|
||||
}
|
||||
|
||||
// Update applies auth progress events and handles the global quit keys.
|
||||
// Update applies runtime progress events and handles global keys.
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case authStartedMsg:
|
||||
@@ -146,158 +93,95 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.streamerUpdates = msg.Updates
|
||||
return m, waitForStreamerUpdate(msg.Updates)
|
||||
case AuthUpdate:
|
||||
if msg.Line != "" {
|
||||
m.authLogs = append(m.authLogs, msg.Line)
|
||||
}
|
||||
if msg.Done {
|
||||
if msg.Err != nil {
|
||||
m.authErr = msg.Err
|
||||
return m, nil
|
||||
}
|
||||
if msg.State != nil {
|
||||
m.authState = msg.State
|
||||
m.authErr = nil
|
||||
m.mode = streamerView
|
||||
m.viewer = nil
|
||||
m.resolveErr = nil
|
||||
m.streamers = loadingEntries(m.streamers)
|
||||
m.ircDetails = make(map[string]ircDetail)
|
||||
m.selectedConfig = ""
|
||||
m.ensureSelection()
|
||||
return m, startStreamerResolution(m.startResolve, m.authState)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if m.authUpdates != nil {
|
||||
return m, waitForAuthUpdate(m.authUpdates)
|
||||
}
|
||||
return m.updateAuth(msg)
|
||||
case StreamerUpdate:
|
||||
if msg.Viewer != nil {
|
||||
m.viewer = msg.Viewer
|
||||
m.resolveErr = nil
|
||||
}
|
||||
if msg.Entry != nil && msg.Index >= 0 && msg.Index < len(m.streamers) {
|
||||
m.streamers[msg.Index] = *msg.Entry
|
||||
}
|
||||
if msg.IRC != nil {
|
||||
m.applyIRCUpdate(*msg.IRC)
|
||||
}
|
||||
if msg.Err != nil {
|
||||
m.resolveErr = msg.Err
|
||||
}
|
||||
m.ensureSelection()
|
||||
if msg.Done {
|
||||
return m, nil
|
||||
}
|
||||
if m.streamerUpdates != nil {
|
||||
return m, waitForStreamerUpdate(m.streamerUpdates)
|
||||
}
|
||||
return m.updateStreamer(msg)
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.resizeComponents()
|
||||
m.ensureSelection()
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.mode == streamerView {
|
||||
m.moveSelection(-1)
|
||||
}
|
||||
return m, nil
|
||||
case "down", "j":
|
||||
if m.mode == streamerView {
|
||||
m.moveSelection(1)
|
||||
}
|
||||
return m, nil
|
||||
case "ctrl+c", "esc", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
if m.mode == authView {
|
||||
var cmd tea.Cmd
|
||||
m.authViewport, cmd = m.authViewport.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// View renders either the login log screen or the authenticated streamer list.
|
||||
func (m Model) View() string {
|
||||
if m.mode == authView {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("Twitch Login\n")
|
||||
if len(m.authLogs) == 0 {
|
||||
builder.WriteString("Starting authentication...\n")
|
||||
} else {
|
||||
for _, line := range m.authLogs {
|
||||
builder.WriteString(line)
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
if m.authErr != nil {
|
||||
builder.WriteString("\nLogin did not complete. Press q to quit.\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
return builder.String()
|
||||
func (m Model) updateAuth(msg AuthUpdate) (tea.Model, tea.Cmd) {
|
||||
if msg.Line != "" {
|
||||
m.authLogs = append(m.authLogs, msg.Line)
|
||||
m.syncAuthViewport()
|
||||
}
|
||||
|
||||
return m.renderStreamerView()
|
||||
if msg.Done {
|
||||
if msg.Err != nil {
|
||||
m.authErr = msg.Err
|
||||
m.syncAuthViewport()
|
||||
return m, nil
|
||||
}
|
||||
if msg.State != nil {
|
||||
m.authState = msg.State
|
||||
m.authErr = nil
|
||||
m.mode = streamerView
|
||||
m.viewer = nil
|
||||
m.resolveErr = nil
|
||||
m.streamers = loadingEntries(m.streamers)
|
||||
m.ircDetails = make(map[string]ircDetail)
|
||||
m.selectedConfig = ""
|
||||
m.ensureSelection()
|
||||
m.resizeComponents()
|
||||
return m, startStreamerResolution(m.runtime, m.authState)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if m.authUpdates != nil {
|
||||
return m, waitForAuthUpdate(m.authUpdates)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) renderStreamerView() string {
|
||||
var builder strings.Builder
|
||||
switch {
|
||||
case m.viewer != nil:
|
||||
fmt.Fprintf(&builder, "Logged in as %s (%s)\n", m.viewer.Login, m.viewer.ID)
|
||||
case m.authState != nil && m.authState.Login != "":
|
||||
fmt.Fprintf(&builder, "Logged in as %s\n", m.authState.Login)
|
||||
if m.resolveErr != nil {
|
||||
fmt.Fprintf(&builder, "Viewer lookup failed: %v\n", m.resolveErr)
|
||||
} else {
|
||||
builder.WriteString("Resolving viewer identity...\n")
|
||||
}
|
||||
func (m Model) updateStreamer(msg StreamerUpdate) (tea.Model, tea.Cmd) {
|
||||
if msg.Viewer != nil {
|
||||
m.viewer = msg.Viewer
|
||||
m.resolveErr = nil
|
||||
}
|
||||
fmt.Fprintf(&builder, "Watching: %s\n", watchingSummary(m.streamers))
|
||||
|
||||
rows := m.orderedRows()
|
||||
selectedIndex := m.selectedRowIndex(rows)
|
||||
leftPane := m.leftPaneLines(rows)
|
||||
rightPane := m.rightPaneLines(rows, selectedIndex)
|
||||
|
||||
if m.height > 0 {
|
||||
available := m.height - 3
|
||||
if available > 0 {
|
||||
leftPane = clipListLines(leftPane, available, selectedIndex+1)
|
||||
rightPane = clipTailLines(rightPane, available)
|
||||
}
|
||||
if msg.Entry != nil && msg.Index >= 0 && msg.Index < len(m.streamers) {
|
||||
m.streamers[msg.Index] = *msg.Entry
|
||||
}
|
||||
|
||||
bodyWidth := m.width
|
||||
if bodyWidth <= 0 {
|
||||
bodyWidth = defaultViewWidth
|
||||
if msg.IRC != nil {
|
||||
m.applyIRCUpdate(*msg.IRC)
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
for _, line := range renderColumns(leftPane, rightPane, bodyWidth) {
|
||||
builder.WriteString(line)
|
||||
builder.WriteByte('\n')
|
||||
if msg.Err != nil {
|
||||
m.resolveErr = msg.Err
|
||||
}
|
||||
builder.WriteByte('\n')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func watchingSummary(streamers []twitch.StreamerEntry) string {
|
||||
live := make([]string, 0, 2)
|
||||
for _, streamer := range streamers {
|
||||
if streamer.Status != twitch.StreamerReady || !streamer.Live {
|
||||
continue
|
||||
}
|
||||
name := streamer.Login
|
||||
if name == "" {
|
||||
name = streamer.ConfigLogin
|
||||
}
|
||||
live = append(live, name)
|
||||
if len(live) == 2 {
|
||||
break
|
||||
}
|
||||
m.ensureSelection()
|
||||
m.resizeComponents()
|
||||
if msg.Done {
|
||||
return m, nil
|
||||
}
|
||||
if len(live) == 0 {
|
||||
return "no live streamers"
|
||||
if m.streamerUpdates != nil {
|
||||
return m, waitForStreamerUpdate(m.streamerUpdates)
|
||||
}
|
||||
return strings.Join(live, ", ")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) applyIRCUpdate(update IRCUpdate) {
|
||||
@@ -314,114 +198,89 @@ func (m *Model) applyIRCUpdate(update IRCUpdate) {
|
||||
detail.joined = false
|
||||
}
|
||||
if update.Line != "" {
|
||||
detail.line = update.Line
|
||||
}
|
||||
m.ircDetails[login] = detail
|
||||
}
|
||||
|
||||
func (m *Model) ensureSelection() {
|
||||
rows := m.orderedRows()
|
||||
if len(rows) == 0 {
|
||||
entries := m.orderedStreamers()
|
||||
if len(entries) == 0 {
|
||||
m.selectedConfig = ""
|
||||
return
|
||||
}
|
||||
if m.selectedConfig == "" {
|
||||
m.selectedConfig = rows[0].entry.ConfigLogin
|
||||
m.selectedConfig = entries[0].ConfigLogin
|
||||
return
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row.entry.ConfigLogin == m.selectedConfig {
|
||||
for _, entry := range entries {
|
||||
if entry.ConfigLogin == m.selectedConfig {
|
||||
return
|
||||
}
|
||||
}
|
||||
m.selectedConfig = rows[0].entry.ConfigLogin
|
||||
m.selectedConfig = entries[0].ConfigLogin
|
||||
}
|
||||
|
||||
func (m *Model) moveSelection(delta int) {
|
||||
rows := m.orderedRows()
|
||||
if len(rows) == 0 {
|
||||
entries := m.orderedStreamers()
|
||||
if len(entries) == 0 {
|
||||
m.selectedConfig = ""
|
||||
return
|
||||
}
|
||||
m.ensureSelection()
|
||||
|
||||
selected := m.selectedRowIndex(rows)
|
||||
selected := m.selectedRowIndex(entries) + delta
|
||||
if selected < 0 {
|
||||
selected = 0
|
||||
}
|
||||
selected += delta
|
||||
if selected < 0 {
|
||||
selected = 0
|
||||
if selected >= len(entries) {
|
||||
selected = len(entries) - 1
|
||||
}
|
||||
if selected >= len(rows) {
|
||||
selected = len(rows) - 1
|
||||
}
|
||||
m.selectedConfig = rows[selected].entry.ConfigLogin
|
||||
m.selectedConfig = entries[selected].ConfigLogin
|
||||
}
|
||||
|
||||
func (m Model) orderedRows() []streamerRow {
|
||||
rows := make([]streamerRow, 0, len(m.streamers))
|
||||
for index, entry := range m.streamers {
|
||||
func (m Model) orderedStreamers() []twitch.StreamerEntry {
|
||||
entries := make([]twitch.StreamerEntry, 0, len(m.streamers))
|
||||
for _, entry := range m.streamers {
|
||||
if isActive(entry) {
|
||||
rows = append(rows, streamerRow{index: index, entry: entry})
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
for index, entry := range m.streamers {
|
||||
for _, entry := range m.streamers {
|
||||
if isActive(entry) {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, streamerRow{index: index, entry: entry})
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return rows
|
||||
return entries
|
||||
}
|
||||
|
||||
func (m Model) selectedRowIndex(rows []streamerRow) int {
|
||||
for index, row := range rows {
|
||||
if row.entry.ConfigLogin == m.selectedConfig {
|
||||
func (m Model) selectedRowIndex(entries []twitch.StreamerEntry) int {
|
||||
for index, entry := range entries {
|
||||
if entry.ConfigLogin == m.selectedConfig {
|
||||
return index
|
||||
}
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
if len(entries) == 0 {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m Model) leftPaneLines(rows []streamerRow) []string {
|
||||
lines := []string{"Streamers"}
|
||||
for _, row := range rows {
|
||||
prefix := " "
|
||||
if row.entry.ConfigLogin == m.selectedConfig {
|
||||
prefix = "> "
|
||||
}
|
||||
|
||||
status := "inactive"
|
||||
if isActive(row.entry) {
|
||||
status = "active"
|
||||
}
|
||||
lines = append(lines, prefix+streamerName(row.entry)+" ["+status+"]")
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
lines = append(lines, " none")
|
||||
}
|
||||
return lines
|
||||
func (m *Model) resizeComponents() {
|
||||
m.authViewport.Width = contentWidth(m.width)
|
||||
m.authViewport.Height = authViewportHeight(m.height)
|
||||
}
|
||||
|
||||
func (m Model) rightPaneLines(rows []streamerRow, selected int) []string {
|
||||
lines := []string{"IRC Chat"}
|
||||
if selected < 0 || selected >= len(rows) {
|
||||
return append(lines, "")
|
||||
}
|
||||
func (m *Model) syncAuthViewport() {
|
||||
m.authViewport.SetContent(m.authLogContent())
|
||||
m.authViewport.GotoBottom()
|
||||
}
|
||||
|
||||
row := rows[selected].entry
|
||||
if !isActive(row) {
|
||||
return append(lines, "inactive")
|
||||
func (m Model) authLogContent() string {
|
||||
if len(m.authLogs) == 0 {
|
||||
return "Starting authentication..."
|
||||
}
|
||||
|
||||
detail := m.ircDetails[normalizeKey(row.Login)]
|
||||
if !detail.joined {
|
||||
return append(lines, "not joined")
|
||||
}
|
||||
return append(lines, greenDot)
|
||||
return strings.Join(m.authLogs, "\n")
|
||||
}
|
||||
|
||||
func isActive(entry twitch.StreamerEntry) bool {
|
||||
@@ -439,140 +298,39 @@ func normalizeKey(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func renderColumns(left, right []string, totalWidth int) []string {
|
||||
if totalWidth <= 0 {
|
||||
totalWidth = defaultViewWidth
|
||||
func loadingEntries(entries []twitch.StreamerEntry) []twitch.StreamerEntry {
|
||||
logins := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
logins = append(logins, entry.ConfigLogin)
|
||||
}
|
||||
|
||||
leftWidth, rightWidth := splitWidths(totalWidth)
|
||||
height := len(left)
|
||||
if len(right) > height {
|
||||
height = len(right)
|
||||
}
|
||||
|
||||
lines := make([]string, 0, height)
|
||||
for index := 0; index < height; index++ {
|
||||
leftLine := ""
|
||||
if index < len(left) {
|
||||
leftLine = left[index]
|
||||
}
|
||||
rightLine := ""
|
||||
if index < len(right) {
|
||||
rightLine = right[index]
|
||||
}
|
||||
lines = append(lines, fitWidth(leftLine, leftWidth)+" │ "+fitWidth(rightLine, rightWidth))
|
||||
}
|
||||
return lines
|
||||
return twitch.LoadingStreamerEntries(logins)
|
||||
}
|
||||
|
||||
func splitWidths(totalWidth int) (int, int) {
|
||||
if totalWidth < 8 {
|
||||
return max(1, totalWidth-4), 1
|
||||
}
|
||||
|
||||
leftWidth := totalWidth / 3
|
||||
if leftWidth < 20 {
|
||||
leftWidth = 20
|
||||
}
|
||||
if leftWidth > 32 {
|
||||
leftWidth = 32
|
||||
}
|
||||
|
||||
rightWidth := totalWidth - leftWidth - 3
|
||||
if rightWidth < 8 {
|
||||
rightWidth = 8
|
||||
leftWidth = totalWidth - rightWidth - 3
|
||||
if leftWidth < 1 {
|
||||
leftWidth = 1
|
||||
rightWidth = max(1, totalWidth-leftWidth-3)
|
||||
}
|
||||
}
|
||||
return leftWidth, rightWidth
|
||||
}
|
||||
|
||||
func fitWidth(value string, width int) string {
|
||||
if width <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
runes := []rune(value)
|
||||
if len(runes) > width {
|
||||
return string(runes[:width])
|
||||
}
|
||||
return value + strings.Repeat(" ", width-len(runes))
|
||||
}
|
||||
|
||||
func clipListLines(lines []string, height, selectedLine int) []string {
|
||||
if height <= 0 || len(lines) <= height {
|
||||
return lines
|
||||
}
|
||||
if height == 1 {
|
||||
return lines[:1]
|
||||
}
|
||||
|
||||
rows := lines[1:]
|
||||
visibleRows := height - 1
|
||||
selectedRow := selectedLine - 1
|
||||
if selectedRow < 0 {
|
||||
selectedRow = 0
|
||||
}
|
||||
start := 0
|
||||
if selectedRow >= visibleRows {
|
||||
start = selectedRow - visibleRows + 1
|
||||
}
|
||||
if start+visibleRows > len(rows) {
|
||||
start = len(rows) - visibleRows
|
||||
}
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
return append([]string{lines[0]}, rows[start:start+visibleRows]...)
|
||||
}
|
||||
|
||||
func clipTailLines(lines []string, height int) []string {
|
||||
if height <= 0 || len(lines) <= height {
|
||||
return lines
|
||||
}
|
||||
if height == 1 {
|
||||
return lines[:1]
|
||||
}
|
||||
|
||||
body := lines[1:]
|
||||
visibleRows := height - 1
|
||||
if len(body) > visibleRows {
|
||||
body = body[len(body)-visibleRows:]
|
||||
}
|
||||
return append([]string{lines[0]}, body...)
|
||||
}
|
||||
|
||||
// startAuthSession starts the background auth worker and returns its update channel to Bubble Tea.
|
||||
func startAuthSession(start StartAuthFunc) tea.Cmd {
|
||||
func startAuthSession(runtime modelRuntime) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if start == nil {
|
||||
return AuthUpdate{Err: errors.New("auth start function is nil"), Done: true}
|
||||
if runtime == nil {
|
||||
return AuthUpdate{Err: errors.New("auth runtime is nil"), Done: true}
|
||||
}
|
||||
updates := make(chan AuthUpdate, 32)
|
||||
start(updates)
|
||||
runtime.startAuth(updates)
|
||||
return authStartedMsg{Updates: updates}
|
||||
}
|
||||
}
|
||||
|
||||
// startStreamerResolution starts the background streamer resolver and returns its update channel.
|
||||
func startStreamerResolution(start StartResolveFunc, state *auth.State) tea.Cmd {
|
||||
func startStreamerResolution(runtime modelRuntime, state *auth.State) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if start == nil {
|
||||
return StreamerUpdate{Err: errors.New("streamer resolution start function is nil"), Done: true}
|
||||
if runtime == nil {
|
||||
return StreamerUpdate{Err: errors.New("streamer runtime is nil"), Done: true}
|
||||
}
|
||||
if state == nil {
|
||||
return StreamerUpdate{Err: errors.New("auth state is nil"), Done: true}
|
||||
}
|
||||
updates := make(chan StreamerUpdate, 32)
|
||||
start(state, updates)
|
||||
runtime.startResolve(state, updates)
|
||||
return streamerStartedMsg{Updates: updates}
|
||||
}
|
||||
}
|
||||
|
||||
// waitForAuthUpdate blocks until the next auth update is available from the worker.
|
||||
func waitForAuthUpdate(updates <-chan AuthUpdate) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
update, ok := <-updates
|
||||
@@ -583,7 +341,6 @@ func waitForAuthUpdate(updates <-chan AuthUpdate) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// waitForStreamerUpdate blocks until the next streamer update is available from the worker.
|
||||
func waitForStreamerUpdate(updates <-chan StreamerUpdate) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
update, ok := <-updates
|
||||
@@ -593,12 +350,3 @@ func waitForStreamerUpdate(updates <-chan StreamerUpdate) tea.Cmd {
|
||||
return update
|
||||
}
|
||||
}
|
||||
|
||||
// loadingEntries resets the UI rows back to their initial loading state.
|
||||
func loadingEntries(entries []twitch.StreamerEntry) []twitch.StreamerEntry {
|
||||
logins := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
logins = append(logins, entry.ConfigLogin)
|
||||
}
|
||||
return twitch.LoadingStreamerEntries(logins)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user