// 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() }