From 49ac8c9e1b0e3e1e8cc828b29690d70ac4fe057b Mon Sep 17 00:00:00 2001 From: Salastil <46979341+Salastil@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:46:20 -0500 Subject: [PATCH] Add date separators in matches list --- internal/app.go | 12 ++++ internal/columns.go | 157 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 150 insertions(+), 19 deletions(-) diff --git a/internal/app.go b/internal/app.go index 3cb19bf..477bc0e 100644 --- a/internal/app.go +++ b/internal/app.go @@ -168,6 +168,18 @@ func New(debug bool) Model { } return fmt.Sprintf("%s %s (%s)", when, title, mt.Category) }) + m.matches.SetSeparator(func(prev, curr Match) (string, bool) { + currDay := time.UnixMilli(curr.Date).Local().Format("Jan 2") + prevDay := "" + if prev.Date != 0 { + prevDay = time.UnixMilli(prev.Date).Local().Format("Jan 2") + } + + if prevDay == "" || prevDay != currDay { + return currDay, true + } + return "", false + }) m.streams = NewListColumn[Stream]("Streams", func(st Stream) string { quality := "SD" if st.HD { diff --git a/internal/columns.go b/internal/columns.go index f216319..cdb8213 100644 --- a/internal/columns.go +++ b/internal/columns.go @@ -49,12 +49,18 @@ type ListColumn[T any] struct { width int height int render renderer[T] + + separator func(prev, curr T) (string, bool) } func NewListColumn[T any](title string, r renderer[T]) *ListColumn[T] { return &ListColumn[T]{title: title, render: r, width: 30, height: 20} } +func (c *ListColumn[T]) SetSeparator(sep func(prev, curr T) (string, bool)) { + c.separator = sep +} + func truncateToWidth(text string, width int) string { if width <= 0 { return "" @@ -77,6 +83,23 @@ func truncateToWidth(text string, width int) string { return text } +func buildSeparatorLine(label string, width int) string { + if width <= 0 { + return label + } + + trimmed := strings.TrimSpace(label) + padded := fmt.Sprintf(" %s ", trimmed) + remaining := width - lipgloss.Width(padded) + if remaining <= 0 { + return truncateToWidth(padded, width) + } + + left := remaining / 2 + right := remaining - left + return strings.Repeat("─", left) + padded + strings.Repeat("─", right) +} + func (c *ListColumn[T]) SetItems(items []T) { c.items = items c.selected = 0 @@ -105,18 +128,14 @@ func (c *ListColumn[T]) CursorUp() { if c.selected > 0 { c.selected-- } - if c.selected < c.scroll { - c.scroll = c.selected - } + c.ensureSelectedVisible() } 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 - } + c.ensureSelectedVisible() } func (c *ListColumn[T]) Selected() (T, bool) { @@ -127,6 +146,80 @@ func (c *ListColumn[T]) Selected() (T, bool) { return c.items[c.selected], true } +type listRow[T any] struct { + text string + isSeparator bool + itemIndex int +} + +func (c *ListColumn[T]) buildRows() []listRow[T] { + rows := make([]listRow[T], 0, len(c.items)) + var prev T + + for i, item := range c.items { + if c.separator != nil { + if sepText, ok := c.separator(prev, item); ok { + rows = append(rows, listRow[T]{text: sepText, isSeparator: true, itemIndex: -1}) + } + } + + rows = append(rows, listRow[T]{text: c.render(item), itemIndex: i}) + prev = item + } + return rows +} + +func (c *ListColumn[T]) clampScroll(totalRows int) { + if c.height <= 0 { + c.scroll = 0 + return + } + + maxScroll := totalRows - c.height + if maxScroll < 0 { + maxScroll = 0 + } + if c.scroll > maxScroll { + c.scroll = maxScroll + } + if c.scroll < 0 { + c.scroll = 0 + } +} + +func (c *ListColumn[T]) ensureSelectedVisible() { + if len(c.items) == 0 { + c.scroll = 0 + return + } + + rows := c.buildRows() + selRow := 0 + for idx, row := range rows { + if row.isSeparator { + continue + } + if row.itemIndex == c.selected { + selRow = idx + break + } + } + + if c.height <= 0 { + c.scroll = selRow + return + } + + if selRow < c.scroll { + c.scroll = selRow + } + if selRow >= c.scroll+c.height { + c.scroll = selRow - c.height + 1 + } + + c.clampScroll(len(rows)) +} + func (c *ListColumn[T]) View(styles Styles, focused bool) string { box := styles.Box if focused { @@ -144,32 +237,58 @@ func (c *ListColumn[T]) View(styles Styles, focused bool) string { if len(c.items) == 0 { lines = append(lines, "(no items)") } else { + rows := c.buildRows() + c.clampScroll(len(rows)) + start := c.scroll end := start + c.height - if end > len(c.items) { - end = len(c.items) + if end > len(rows) { + end = len(rows) } - meta = styles.Subtle.Render(fmt.Sprintf("Showing %d–%d of %d", start+1, end, len(c.items))) + + startItem, endItem := -1, -1 + for i := start; i < end; i++ { + row := rows[i] cursor := " " - lineText := c.render(c.items[i]) + lineText := row.text contentWidth := c.width - lipgloss.Width(cursor) - if contentWidth > 1 && lipgloss.Width(lineText) > contentWidth { - lineText = fmt.Sprintf("%s…", truncateToWidth(lineText, contentWidth-1)) - } - if i == c.selected { - cursor = "▸ " - lineText = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FA8072")). // Not pink, its Salmon obviously - Bold(true). - Render(lineText) + if row.isSeparator { + lineText = buildSeparatorLine(lineText, contentWidth) + lineText = styles.Subtle.Render(lineText) + } else { + if contentWidth > 1 && lipgloss.Width(lineText) > contentWidth { + lineText = fmt.Sprintf("%s…", truncateToWidth(lineText, contentWidth-1)) + } + + if startItem == -1 { + startItem = row.itemIndex + } + endItem = row.itemIndex + + if row.itemIndex == c.selected { + cursor = "▸ " + lineText = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FA8072")). // Not pink, its Salmon obviously + Bold(true). + Render(lineText) + } } line := fmt.Sprintf("%s%s", cursor, lineText) lines = append(lines, line) } + + if startItem == -1 { + startItem = 0 + } + if endItem == -1 { + endItem = startItem + } + + meta = styles.Subtle.Render(fmt.Sprintf("Showing %d–%d of %d", startItem+1, endItem+1, len(c.items))) } // Fill remaining lines if fewer than height