internal/tui/app.go

// Package tui is the interactive tview front-end.
//
// See mercemay.top/src/portr/ for context.
package tui

import (
	"context"
	"fmt"

	"github.com/rivo/tview"

	"github.com/mercemay/portr/internal/check"
	"github.com/mercemay/portr/internal/config"
	"github.com/mercemay/portr/internal/tui/view"
)

// App wires the top-level layout together.
type App struct {
	app    *tview.Application
	table  *view.Table
	detail *view.Detail
	filter *view.Filter
	status *tview.TextView
	cfg    config.Config
}

// New builds an App, ready to call Run.
func New(cfg config.Config) *App {
	a := &App{app: tview.NewApplication(), cfg: cfg}
	a.table = view.NewTable()
	a.detail = view.NewDetail()
	a.filter = view.NewFilter(func(q string) { a.table.Filter(q) })
	a.status = tview.NewTextView().SetDynamicColors(true)
	a.status.SetBorder(true).SetTitle(" portr ")

	layout := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(a.filter.View(), 1, 0, false).
		AddItem(tview.NewFlex().
			AddItem(a.table.View(), 0, 3, true).
			AddItem(a.detail.View(), 0, 2, false), 0, 1, true).
		AddItem(a.status, 3, 0, false)
	a.app.SetRoot(layout, true).EnableMouse(true)
	a.table.OnSelect(a.detail.Show)
	bindKeys(a)
	return a
}

// Run kicks off one check synchronously, then enters the event loop.
func (a *App) Run(ctx context.Context) error {
	rep, err := check.Run(ctx, a.cfg)
	if err != nil {
		return err
	}
	a.applyReport(rep)
	return a.app.Run()
}

func (a *App) applyReport(rep check.Report) {
	a.table.Load(rep.Results)
	open, closed := rep.Summary()
	a.status.SetText(fmt.Sprintf(" [green]%d open[-] / [red]%d closed[-]  (took %s)",
		open, closed, rep.Finished.Sub(rep.Started)))
}

// Stop quits the tview event loop.
func (a *App) Stop() { a.app.Stop() }