feat: add twitch tv login flow and auth state ui
This commit is contained in:
+127
-7
@@ -1,28 +1,106 @@
|
||||
// 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"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"parasocial/internal/auth"
|
||||
)
|
||||
|
||||
// Model is the initial terminal UI for parasocial.
|
||||
type viewMode int
|
||||
|
||||
const (
|
||||
authView viewMode = iota
|
||||
streamerView
|
||||
)
|
||||
|
||||
type StartAuthFunc func(chan<- AuthUpdate)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Options configures the initial streamer list, auth state, and auth starter hook.
|
||||
type Options struct {
|
||||
Streamers []string
|
||||
AuthState *auth.State
|
||||
StartAuth StartAuthFunc
|
||||
}
|
||||
|
||||
// authStartedMsg hands the model the channel that will stream auth updates.
|
||||
type authStartedMsg struct {
|
||||
Updates <-chan AuthUpdate
|
||||
}
|
||||
|
||||
// Model is the terminal UI for auth and streamer display.
|
||||
type Model struct {
|
||||
streamers []string
|
||||
streamers []string
|
||||
authState *auth.State
|
||||
startAuth StartAuthFunc
|
||||
authUpdates <-chan AuthUpdate
|
||||
authLogs []string
|
||||
authErr error
|
||||
mode viewMode
|
||||
}
|
||||
|
||||
// New returns a Bubble Tea model that displays configured streamers.
|
||||
func New(streamers []string) Model {
|
||||
return Model{streamers: append([]string(nil), streamers...)}
|
||||
// New returns a Bubble Tea model for auth and streamer display.
|
||||
func New(options Options) Model {
|
||||
model := Model{
|
||||
streamers: append([]string(nil), options.Streamers...),
|
||||
authState: options.AuthState,
|
||||
startAuth: options.StartAuth,
|
||||
authLogs: []string{},
|
||||
mode: authView,
|
||||
}
|
||||
if options.AuthState != nil {
|
||||
model.mode = streamerView
|
||||
}
|
||||
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 nil
|
||||
}
|
||||
|
||||
// Update applies auth progress events and handles the global quit keys.
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case authStartedMsg:
|
||||
m.authUpdates = msg.Updates
|
||||
return m, waitForAuthUpdate(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
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if m.authUpdates != nil {
|
||||
return m, waitForAuthUpdate(m.authUpdates)
|
||||
}
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc", "q":
|
||||
@@ -33,14 +111,56 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// View renders either the login log screen or the authenticated streamer list.
|
||||
func (m Model) View() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("Streamers\n")
|
||||
if m.mode == authView {
|
||||
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()
|
||||
}
|
||||
|
||||
if m.authState != nil && m.authState.Login != "" {
|
||||
fmt.Fprintf(&builder, "Logged in as %s\n\n", m.authState.Login)
|
||||
}
|
||||
builder.WriteString("Streamers\n")
|
||||
for i, streamer := range m.streamers {
|
||||
fmt.Fprintf(&builder, "%d. %s\n", i+1, streamer)
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// startAuthSession starts the background auth worker and returns its update channel to Bubble Tea.
|
||||
func startAuthSession(start StartAuthFunc) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if start == nil {
|
||||
return AuthUpdate{Err: errors.New("auth start function is nil"), Done: true}
|
||||
}
|
||||
updates := make(chan AuthUpdate, 32)
|
||||
start(updates)
|
||||
return authStartedMsg{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
|
||||
if !ok {
|
||||
return AuthUpdate{Err: errors.New("auth updates ended unexpectedly"), Done: true}
|
||||
}
|
||||
return update
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user