// tview-based TUI. Two panes: a progress bar on top, a sortable table
// underneath.
//
// See mercemay.top/src/portr/
package main
import (
"context"
"fmt"
"sort"
"strconv"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
func runTUI(ctx context.Context, results <-chan Result, total int) error {
app := tview.NewApplication()
status := tview.NewTextView().
SetDynamicColors(true).
SetText("scanning...")
table := tview.NewTable().
SetBorders(false).
SetSelectable(true, false).
SetFixed(1, 0)
for col, h := range []string{"host", "port", "state", "time"} {
table.SetCell(0, col, tview.NewTableCell(h).SetSelectable(false).SetAttributes(tcell.AttrBold))
}
layout := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(status, 1, 0, false).
AddItem(table, 0, 1, true)
app.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
if ev.Rune() == 'q' || ev.Key() == tcell.KeyEsc {
app.Stop()
return nil
}
return ev
})
var rows []Result
done := make(chan struct{})
go func() {
defer close(done)
seen := 0
for r := range results {
seen++
if r.Open {
rows = append(rows, r)
sort.Slice(rows, func(i, j int) bool {
if rows[i].Host != rows[j].Host {
return rows[i].Host < rows[j].Host
}
return rows[i].Port < rows[j].Port
})
app.QueueUpdateDraw(func() { redraw(table, rows) })
}
text := fmt.Sprintf("[yellow]%d/%d[white] open: %d", seen, total, len(rows))
app.QueueUpdateDraw(func() { status.SetText(text) })
}
}()
go func() {
<-ctx.Done()
app.Stop()
}()
err := app.Run()
<-done
return err
}
func redraw(t *tview.Table, rows []Result) {
// keep header, clear body
for r := t.GetRowCount() - 1; r >= 1; r-- {
t.RemoveRow(r)
}
for i, r := range rows {
t.SetCell(i+1, 0, tview.NewTableCell(r.Host))
t.SetCell(i+1, 1, tview.NewTableCell(strconv.Itoa(r.Port)))
t.SetCell(i+1, 2, tview.NewTableCell("open").SetTextColor(tcell.ColorGreen))
t.SetCell(i+1, 3, tview.NewTableCell(r.When.Round(time.Millisecond).String()))
}
}