tui.go

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