tsplay/internal/tui/login.go
28allday 8ee58b905f Initial commit: tuistream-play (tsplay) v0.1
Terminal Jellyfin client, watch-side companion to TUISTREAM.

- LAN auto-discovery (broadcast + unicast subnet sweep)
- mpv-window video playback over JSON IPC (pause/seek/progress/resume)
- headless audio playback with in-terminal now-playing screen
- volume control with remembered level, mute
- audio auto-advance + n/p track skip through the album queue
- Omarchy-themed UI with Nerd Font (nf-md) icons
- cliamp Jellyfin provider auto-config on login
- pacman dependency-installing installer

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:36:49 +01:00

118 lines
3 KiB
Go

package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"tuistream-play/internal/discovery"
)
func (m Model) updateLogin(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok {
switch key.String() {
case "ctrl+c", "esc":
return m, tea.Quit
case "ctrl+n":
// Cycle through discovered servers, filling the URL field.
if len(m.discovered) > 1 {
m.discIdx = (m.discIdx + 1) % len(m.discovered)
m.inputs[0].SetValue(m.discovered[m.discIdx].Address)
m.status = discoveryStatus(m.discovered, m.discIdx)
}
return m, nil
case "ctrl+r":
m.status = "Scanning the network for Jellyfin servers…"
return m, discoverCmd()
case "tab", "down":
m.focusField(m.focus + 1)
return m, textinput.Blink
case "shift+tab", "up":
m.focusField(m.focus - 1)
return m, textinput.Blink
case "enter":
server := strings.TrimSpace(m.inputs[0].Value())
user := strings.TrimSpace(m.inputs[1].Value())
pass := m.inputs[2].Value()
if server == "" || user == "" {
m.status = "Server URL and username are required."
return m, nil
}
if m.logging {
return m, nil
}
m.logging = true
m.status = "Connecting…"
return m, m.doLogin(server, user, pass)
}
}
// Feed the event to every input so the cursor blinks and the focused one
// receives text.
var cmds []tea.Cmd
for i := range m.inputs {
var c tea.Cmd
m.inputs[i], c = m.inputs[i].Update(msg)
cmds = append(cmds, c)
}
return m, tea.Batch(cmds...)
}
func (m *Model) focusField(i int) {
n := len(m.inputs)
i = ((i % n) + n) % n // wrap
for j := range m.inputs {
if j == i {
m.inputs[j].Focus()
} else {
m.inputs[j].Blur()
}
}
m.focus = i
}
// discoveryStatus formats the "found N servers" line shown under the form.
func discoveryStatus(servers []discovery.Server, idx int) string {
s := servers[idx]
name := s.Name
if name == "" {
name = s.Address
}
if len(servers) == 1 {
return "Found: " + name
}
return fmt.Sprintf("Found %d servers — using %s [%d/%d]", len(servers), name, idx+1, len(servers))
}
func (m Model) viewLogin() string {
var b strings.Builder
b.WriteString(m.st.title.Render("tsplay") + m.st.muted.Render(" — Jellyfin terminal player"))
b.WriteString("\n\n")
for i := range m.inputs {
b.WriteString(m.inputs[i].View())
b.WriteString("\n")
}
b.WriteString("\n")
help := "tab move · enter sign in · esc quit"
if len(m.discovered) > 1 {
help = "tab move · ctrl+n next server · enter sign in · esc quit"
} else {
help += " · ctrl+r rescan"
}
b.WriteString(m.st.help.Render(help))
body := m.st.box.Render(b.String())
// Discovery messages are informational (accent); real errors stay red.
footer := ""
if m.status != "" {
style := m.st.accent
if strings.HasPrefix(m.status, "Login failed") || strings.HasPrefix(m.status, "No servers") {
style = m.st.bad
}
footer = "\n" + style.Render(m.status)
}
return lipgloss.JoinVertical(lipgloss.Left, body, footer)
}