Fixed bad import, columns don't expand vertically with the length of the list
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -30,3 +30,5 @@ go.work.sum
|
|||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
go.sum
|
||||||
|
streamed-tui
|
||||||
|
|||||||
23
go.mod
23
go.mod
@@ -6,4 +6,25 @@ require (
|
|||||||
github.com/charmbracelet/bubbles v0.18.0
|
github.com/charmbracelet/bubbles v0.18.0
|
||||||
github.com/charmbracelet/bubbletea v0.26.6
|
github.com/charmbracelet/bubbletea v0.26.6
|
||||||
github.com/charmbracelet/lipgloss v0.13.0
|
github.com/charmbracelet/lipgloss v0.13.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.1.4 // indirect
|
||||||
|
github.com/charmbracelet/x/input v0.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||||
|
github.com/charmbracelet/x/windows v0.1.0 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.21.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import (
|
|||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
"github.com/Salastil/streamed-tui/internal/api"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
// KEY MAP
|
// KEY MAP + HELP
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
|
//
|
||||||
|
|
||||||
type keyMap struct {
|
type keyMap struct {
|
||||||
Up, Down, Left, Right key.Binding
|
Up, Down, Left, Right key.Binding
|
||||||
@@ -36,9 +36,39 @@ func defaultKeys() keyMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// implement help.KeyMap interface
|
||||||
|
func (k keyMap) ShortHelp() []key.Binding {
|
||||||
|
return []key.Binding{k.Up, k.Down, k.Left, k.Right, k.Enter, k.Quit}
|
||||||
|
}
|
||||||
|
func (k keyMap) FullHelp() [][]key.Binding {
|
||||||
|
return [][]key.Binding{
|
||||||
|
{k.Up, k.Down, k.Left, k.Right},
|
||||||
|
{k.Enter, k.Refresh, k.Quit},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ────────────────────────────────
|
||||||
|
// MESSAGE TYPES
|
||||||
|
// ────────────────────────────────
|
||||||
|
//
|
||||||
|
|
||||||
|
type (
|
||||||
|
sportsLoadedMsg []Sport
|
||||||
|
matchesLoadedMsg struct {
|
||||||
|
Matches []Match
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
streamsLoadedMsg []Stream
|
||||||
|
errorMsg error
|
||||||
|
launchStreamMsg struct{ URL string }
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
// MODEL
|
// MODEL
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
|
//
|
||||||
|
|
||||||
type focusCol int
|
type focusCol int
|
||||||
|
|
||||||
@@ -49,23 +79,25 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
apiClient *api.Client
|
apiClient *Client
|
||||||
|
|
||||||
styles Styles
|
styles Styles
|
||||||
keys keyMap
|
keys keyMap
|
||||||
help help.Model
|
help help.Model
|
||||||
focus focusCol
|
focus focusCol
|
||||||
|
|
||||||
sports *ListColumn[api.Sport]
|
sports *ListColumn[Sport]
|
||||||
matches *ListColumn[api.Match]
|
matches *ListColumn[Match]
|
||||||
streams *ListColumn[api.Stream]
|
streams *ListColumn[Stream]
|
||||||
|
|
||||||
status string
|
status string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
// APP ENTRYPOINT
|
// APP ENTRYPOINT
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
|
//
|
||||||
|
|
||||||
func Run() error {
|
func Run() error {
|
||||||
p := tea.NewProgram(New(), tea.WithAltScreen())
|
p := tea.NewProgram(New(), tea.WithAltScreen())
|
||||||
@@ -74,8 +106,8 @@ func Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
base := api.BaseURLFromEnv()
|
base := BaseURLFromEnv()
|
||||||
client := api.NewClient(base, 15*time.Second)
|
client := NewClient(base, 15*time.Second)
|
||||||
|
|
||||||
styles := NewStyles()
|
styles := NewStyles()
|
||||||
m := Model{
|
m := Model{
|
||||||
@@ -86,8 +118,8 @@ func New() Model {
|
|||||||
focus: focusSports,
|
focus: focusSports,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.sports = NewListColumn[api.Sport]("Sports", func(s api.Sport) string { return s.Name })
|
m.sports = NewListColumn[Sport]("Sports", func(s Sport) string { return s.Name })
|
||||||
m.matches = NewListColumn[api.Match]("Popular Matches", func(mt api.Match) string {
|
m.matches = NewListColumn[Match]("Popular Matches", func(mt Match) string {
|
||||||
when := time.UnixMilli(mt.Date).Local().Format("Jan 2 15:04")
|
when := time.UnixMilli(mt.Date).Local().Format("Jan 2 15:04")
|
||||||
title := mt.Title
|
title := mt.Title
|
||||||
if mt.Teams != nil && mt.Teams.Home != nil && mt.Teams.Away != nil {
|
if mt.Teams != nil && mt.Teams.Home != nil && mt.Teams.Away != nil {
|
||||||
@@ -95,7 +127,7 @@ func New() Model {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%s %s (%s)", when, title, mt.Category)
|
return fmt.Sprintf("%s %s (%s)", when, title, mt.Category)
|
||||||
})
|
})
|
||||||
m.streams = NewListColumn[api.Stream]("Streams", func(st api.Stream) string {
|
m.streams = NewListColumn[Stream]("Streams", func(st Stream) string {
|
||||||
quality := "SD"
|
quality := "SD"
|
||||||
if st.HD {
|
if st.HD {
|
||||||
quality = "HD"
|
quality = "HD"
|
||||||
@@ -111,9 +143,11 @@ func (m Model) Init() tea.Cmd {
|
|||||||
return tea.Batch(m.fetchSports(), m.fetchPopularMatches())
|
return tea.Batch(m.fetchSports(), m.fetchPopularMatches())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
// VIEW
|
// VIEW
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
|
//
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
cols := lipgloss.JoinHorizontal(
|
cols := lipgloss.JoinHorizontal(
|
||||||
@@ -126,9 +160,11 @@ func (m Model) View() string {
|
|||||||
return lipgloss.JoinVertical(lipgloss.Left, cols, status, m.help.View(m.keys))
|
return lipgloss.JoinVertical(lipgloss.Left, cols, status, m.help.View(m.keys))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
// UPDATE
|
// UPDATE
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
|
//
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
@@ -221,9 +257,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
// COMMANDS
|
// COMMANDS
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
|
//
|
||||||
|
|
||||||
func (m Model) fetchSports() tea.Cmd {
|
func (m Model) fetchSports() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
@@ -245,7 +283,7 @@ func (m Model) fetchPopularMatches() tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) fetchMatchesForSport(s api.Sport) tea.Cmd {
|
func (m Model) fetchMatchesForSport(s Sport) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
matches, err := m.apiClient.GetMatchesBySport(context.Background(), s.ID)
|
matches, err := m.apiClient.GetMatchesBySport(context.Background(), s.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -255,7 +293,7 @@ func (m Model) fetchMatchesForSport(s api.Sport) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) fetchStreamsForMatch(mt api.Match) tea.Cmd {
|
func (m Model) fetchStreamsForMatch(mt Match) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
streams, err := m.apiClient.GetStreamsForMatch(context.Background(), mt)
|
streams, err := m.apiClient.GetStreamsForMatch(context.Background(), mt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -265,7 +303,7 @@ func (m Model) fetchStreamsForMatch(mt api.Match) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) launchMPV(st api.Stream) tea.Cmd {
|
func (m Model) launchMPV(st Stream) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
url := st.EmbedURL
|
url := st.EmbedURL
|
||||||
if url == "" {
|
if url == "" {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
@@ -28,7 +29,7 @@ func NewStyles() Styles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
// GENERIC LIST COLUMN
|
// GENERIC LIST COLUMN (SCROLLABLE)
|
||||||
// ────────────────────────────────
|
// ────────────────────────────────
|
||||||
|
|
||||||
type renderer[T any] func(T) string
|
type renderer[T any] func(T) string
|
||||||
@@ -37,19 +38,43 @@ type ListColumn[T any] struct {
|
|||||||
title string
|
title string
|
||||||
items []T
|
items []T
|
||||||
selected int
|
selected int
|
||||||
|
scroll int
|
||||||
width int
|
width int
|
||||||
|
height int
|
||||||
render renderer[T]
|
render renderer[T]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListColumn[T any](title string, r renderer[T]) *ListColumn[T] {
|
func NewListColumn[T any](title string, r renderer[T]) *ListColumn[T] {
|
||||||
return &ListColumn[T]{title: title, render: r, width: 30}
|
return &ListColumn[T]{title: title, render: r, width: 30, height: 20}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ListColumn[T]) SetItems(items []T) {
|
||||||
|
c.items = items
|
||||||
|
c.selected = 0
|
||||||
|
c.scroll = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ListColumn[T]) SetItems(items []T) { c.items = items; c.selected = 0 }
|
|
||||||
func (c *ListColumn[T]) SetTitle(title string) { c.title = title }
|
func (c *ListColumn[T]) SetTitle(title string) { c.title = title }
|
||||||
func (c *ListColumn[T]) SetWidth(w int) { if w > 20 { c.width = w - 2 } }
|
func (c *ListColumn[T]) SetWidth(w int) { if w > 20 { c.width = w - 2 } }
|
||||||
func (c *ListColumn[T]) CursorUp() { if c.selected > 0 { c.selected-- } }
|
func (c *ListColumn[T]) SetHeight(h int) { if h > 5 { c.height = h - 5 } }
|
||||||
func (c *ListColumn[T]) CursorDown() { if c.selected < len(c.items)-1 { c.selected++ } }
|
|
||||||
|
func (c *ListColumn[T]) CursorUp() {
|
||||||
|
if c.selected > 0 {
|
||||||
|
c.selected--
|
||||||
|
}
|
||||||
|
if c.selected < c.scroll {
|
||||||
|
c.scroll = c.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ListColumn[T]) CursorDown() {
|
||||||
|
if c.selected < len(c.items)-1 {
|
||||||
|
c.selected++
|
||||||
|
}
|
||||||
|
if c.selected >= c.scroll+c.height {
|
||||||
|
c.scroll = c.selected - c.height + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ListColumn[T]) Selected() (T, bool) {
|
func (c *ListColumn[T]) Selected() (T, bool) {
|
||||||
var zero T
|
var zero T
|
||||||
@@ -66,17 +91,34 @@ func (c *ListColumn[T]) View(styles Styles, focused bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
head := styles.Title.Render(c.title)
|
head := styles.Title.Render(c.title)
|
||||||
content := ""
|
lines := []string{}
|
||||||
for i, it := range c.items {
|
|
||||||
cursor := " "
|
if len(c.items) == 0 {
|
||||||
if i == c.selected {
|
lines = append(lines, "(no items)")
|
||||||
cursor = "▸ "
|
} else {
|
||||||
|
start := c.scroll
|
||||||
|
end := start + c.height
|
||||||
|
if end > len(c.items) {
|
||||||
|
end = len(c.items)
|
||||||
}
|
}
|
||||||
line := fmt.Sprintf("%s%s", cursor, c.render(it))
|
for i := start; i < end; i++ {
|
||||||
if len(line) > c.width && c.width > 3 {
|
cursor := " "
|
||||||
line = line[:c.width-3] + "…"
|
if i == c.selected {
|
||||||
|
cursor = "▸ "
|
||||||
|
}
|
||||||
|
line := fmt.Sprintf("%s%s", cursor, c.render(c.items[i]))
|
||||||
|
if len(line) > c.width && c.width > 3 {
|
||||||
|
line = line[:c.width-3] + "…"
|
||||||
|
}
|
||||||
|
lines = append(lines, line)
|
||||||
}
|
}
|
||||||
content += line + "\n"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill remaining lines if fewer than height
|
||||||
|
for len(lines) < c.height {
|
||||||
|
lines = append(lines, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
content := strings.Join(lines, "\n")
|
||||||
return box.Width(c.width + 2).Render(head + "\n" + content)
|
return box.Width(c.width + 2).Render(head + "\n" + content)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user