tsplay/internal/tui/frame.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

67 lines
2 KiB
Go

// Shared screen chrome: a header rule (app name + server) and a footer rule
// (status + help) used by the browse and playing screens so they feel like one
// app rather than three separate views.
package tui
import (
"net/url"
"strings"
"github.com/charmbracelet/lipgloss"
)
// hostLabel extracts a friendly host from a server URL — no scheme or port, and
// a trailing ".local" trimmed (so "http://omabox.local:8096" → "omabox").
func hostLabel(raw string) string {
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return raw
}
return strings.TrimSuffix(u.Hostname(), ".local")
}
// serverLabel prefers a discovered server's advertised name (matched by URL),
// otherwise falls back to the host portion of the configured URL.
func (m Model) serverLabel() string {
for _, s := range m.discovered {
if s.Address == m.cfg.ServerURL && s.Name != "" {
return s.Name
}
}
return hostLabel(m.cfg.ServerURL)
}
// frameWidth returns the usable width, with a sane fallback before the first
// WindowSizeMsg arrives.
func (m Model) frameWidth() int {
if m.width <= 0 {
return 80
}
return m.width
}
// headerBar renders the top rule: " tsplay ───────────────── server ".
func (m Model) headerBar() string {
title := m.st.title.Render(" tsplay ")
srv := ""
if s := m.serverLabel(); s != "" {
srv = m.st.muted.Render(" " + s + " ")
}
dash := m.frameWidth() - lipgloss.Width(title) - lipgloss.Width(srv)
if dash < 1 {
dash = 1
}
return title + m.st.barEmpty.Render(strings.Repeat("─", dash)) + srv
}
// footerBar renders a thin rule, an optional accent status line, then the help
// keys — the consistent bottom of every framed screen.
func (m Model) footerBar(help, status string) string {
rule := m.st.barEmpty.Render(strings.Repeat("─", m.frameWidth()))
lines := []string{rule}
if status != "" {
lines = append(lines, " "+m.st.accent.Render(status))
}
lines = append(lines, " "+m.st.help.Render(help))
return strings.Join(lines, "\n")
}