156 lines
4.2 KiB
Go
156 lines
4.2 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker"
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/gh"
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/logs"
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/registry"
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/task"
|
|
)
|
|
|
|
// loadedMsg carries a full refresh. Every source is fetched concurrently and
|
|
// each failure is reported separately, so a missing GitHub CLI degrades the
|
|
// pull-request list without hiding local images.
|
|
type loadedMsg struct {
|
|
generation int
|
|
prs []gh.PR
|
|
images []docker.Image
|
|
containers []docker.Container
|
|
volumes []docker.Volume
|
|
tags map[string]bool
|
|
prErr error
|
|
imageErr error
|
|
tagErr error
|
|
}
|
|
|
|
// containersMsg is the cheap periodic refresh; it skips GitHub and the registry.
|
|
type containersMsg struct {
|
|
generation int
|
|
containers []docker.Container
|
|
volumes []docker.Volume
|
|
}
|
|
|
|
type (
|
|
tasksChangedMsg struct{}
|
|
logsChangedMsg struct{}
|
|
ciChecksMsg struct {
|
|
pr int
|
|
checks []gh.Check
|
|
err error
|
|
}
|
|
pollCIChecksMsg struct {
|
|
pr int
|
|
}
|
|
frameMsg time.Time
|
|
refreshMsg time.Time
|
|
statusMsg struct {
|
|
text string
|
|
level statusLevel
|
|
}
|
|
)
|
|
|
|
const (
|
|
loadTimeout = 45 * time.Second
|
|
refreshEvery = 3 * time.Second
|
|
frameEvery = 90 * time.Millisecond
|
|
prListingLimit = 60
|
|
)
|
|
|
|
// loadAll fetches every data source in parallel and reports them as one
|
|
// message, so the table is only rebuilt once per refresh.
|
|
func loadAll(generation int, includeBots, refresh bool) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ctx, cancel := context.WithTimeout(context.Background(), loadTimeout)
|
|
defer cancel()
|
|
|
|
var (
|
|
group sync.WaitGroup
|
|
message = loadedMsg{generation: generation}
|
|
)
|
|
group.Add(3)
|
|
go func() {
|
|
defer group.Done()
|
|
if refresh {
|
|
message.prs, message.prErr = gh.RefreshPRs(ctx, prListingLimit, includeBots)
|
|
return
|
|
}
|
|
message.prs, message.prErr = gh.ListPRs(ctx, prListingLimit, includeBots)
|
|
}()
|
|
go func() {
|
|
defer group.Done()
|
|
message.images, message.imageErr = docker.ListLocalImages(ctx)
|
|
}()
|
|
go func() {
|
|
defer group.Done()
|
|
message.tags, message.tagErr = registry.PublishedTags(ctx, refresh)
|
|
}()
|
|
group.Wait()
|
|
|
|
message.containers, _ = docker.List()
|
|
message.volumes, _ = docker.ListVolumes(ctx, true)
|
|
return message
|
|
}
|
|
}
|
|
|
|
// loadContainers is the lightweight poll that keeps running state current
|
|
// without paying for GitHub, registry or disk-usage round trips.
|
|
func loadContainers(generation int) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
containers, _ := docker.List()
|
|
volumes, _ := docker.ListVolumes(ctx, false)
|
|
return containersMsg{generation: generation, containers: containers, volumes: volumes}
|
|
}
|
|
}
|
|
|
|
// waitForTasks parks a goroutine on the task manager's coalescing wake-up
|
|
// channel. Exactly one of these is in flight at a time; each wake-up re-arms
|
|
// itself, which is what lets background work drive redraws without polling.
|
|
func waitForTasks(manager *task.Manager) tea.Cmd {
|
|
return func() tea.Msg {
|
|
<-manager.Changed()
|
|
return tasksChangedMsg{}
|
|
}
|
|
}
|
|
|
|
// waitForLogs mirrors waitForTasks for the live container log streams.
|
|
func waitForLogs(registry *logs.Registry) tea.Cmd {
|
|
return func() tea.Msg {
|
|
<-registry.Changed()
|
|
return logsChangedMsg{}
|
|
}
|
|
}
|
|
|
|
func tickRefresh() tea.Cmd {
|
|
return tea.Tick(refreshEvery, func(now time.Time) tea.Msg { return refreshMsg(now) })
|
|
}
|
|
|
|
// tickFrame drives spinner and progress-bar animation. It only runs while
|
|
// something is actually moving, so an idle CLI costs no wake-ups at all.
|
|
func tickFrame() tea.Cmd {
|
|
return tea.Tick(frameEvery, func(now time.Time) tea.Msg { return frameMsg(now) })
|
|
}
|
|
|
|
func notify(text string, level statusLevel) tea.Cmd {
|
|
return func() tea.Msg { return statusMsg{text: text, level: level} }
|
|
}
|
|
|
|
func fetchCIChecks(pr int, refresh bool) tea.Cmd {
|
|
if pr <= 0 {
|
|
return nil
|
|
}
|
|
return func() tea.Msg {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
checks, err := gh.GetPRChecks(ctx, pr, refresh)
|
|
return ciChecksMsg{pr: pr, checks: checks, err: err}
|
|
}
|
|
}
|