Commit dc75442

Ansari <ping@ansari.wtf>
2026-02-23 11:36:13
initial commit
cmd/ufWall/main.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"ufWall/internal/app"
+
+	tea "github.com/charmbracelet/bubbletea"
+)
+
+func main() {
+	p := tea.NewProgram(app.InitialModel(), tea.WithAltScreen())
+	if _, err := p.Run(); err != nil {
+		fmt.Printf("Error: %v\n", err)
+		os.Exit(1)
+	}
+}
internal/app/keys.go
@@ -0,0 +1,19 @@
+package app
+
+import "github.com/charmbracelet/bubbles/key"
+
+type KeyMap struct {
+	Quit    key.Binding
+	Refresh key.Binding
+}
+
+var Keys = KeyMap{
+	Quit: key.NewBinding(
+		key.WithKeys("q", "ctrl+c", "esc"),
+		key.WithHelp("q/esc", "quit"),
+	),
+	Refresh: key.NewBinding(
+		key.WithKeys("r"),
+		key.WithHelp("r", "refresh"),
+	),
+}
internal/app/model.go
@@ -0,0 +1,19 @@
+package app
+
+import "ufWall/internal/ufw"
+
+type model struct {
+	status ufw.UFWStatus
+	styles Styles
+	width  int
+	height int
+}
+
+func InitialModel() model {
+	return model{
+		status: ufw.GetStatus(),
+		styles: NewStyles(),
+		width:  80,
+		height: 24,
+	}
+}
internal/app/sections.go
@@ -0,0 +1,116 @@
+package app
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/charmbracelet/lipgloss"
+)
+
+func (m model) renderStatusSection() string {
+	var statusText string
+	var statusStyle lipgloss.Style
+
+	if m.status.Active {
+		statusText = "ACTIVE"
+		statusStyle = m.styles.ActiveStatus
+	} else {
+		statusText = "INACTIVE"
+		statusStyle = m.styles.InactiveStatus
+	}
+
+	statusLine := lipgloss.JoinHorizontal(
+		lipgloss.Left,
+		m.styles.Label.Render("Status:"),
+		statusStyle.Render(statusText),
+	)
+
+	loggingLine := lipgloss.JoinHorizontal(
+		lipgloss.Left,
+		m.styles.Label.Render("Logging:"),
+		m.styles.Value.Render(strings.ToUpper(m.status.Logging)),
+	)
+
+	content := lipgloss.JoinVertical(
+		lipgloss.Left,
+		statusLine,
+		loggingLine,
+	)
+
+	return m.styles.SectionBorder.Render(content)
+}
+
+func (m model) renderPoliciesSection() string {
+	title := m.styles.SectionTitle.Render("Default Policies")
+
+	incomingStyle := m.getPolicyStyle(m.status.DefaultIn)
+	outgoingStyle := m.getPolicyStyle(m.status.DefaultOut)
+	routedStyle := m.getPolicyStyle(m.status.DefaultRouted)
+
+	incomingLine := lipgloss.JoinHorizontal(
+		lipgloss.Left,
+		m.styles.Label.Render("Incoming:"),
+		incomingStyle.Render(m.status.DefaultIn),
+	)
+
+	outgoingLine := lipgloss.JoinHorizontal(
+		lipgloss.Left,
+		m.styles.Label.Render("Outgoing:"),
+		outgoingStyle.Render(m.status.DefaultOut),
+	)
+
+	routedLine := lipgloss.JoinHorizontal(
+		lipgloss.Left,
+		m.styles.Label.Render("Routed:"),
+		routedStyle.Render(m.status.DefaultRouted),
+	)
+
+	content := lipgloss.JoinVertical(
+		lipgloss.Left,
+		title,
+		incomingLine,
+		outgoingLine,
+		routedLine,
+	)
+
+	return m.styles.SectionBorder.Render(content)
+}
+
+func (m model) renderRulesSection() string {
+	title := m.styles.SectionTitle.Render("Active Rules")
+
+	ruleCountText := fmt.Sprintf("%d rule(s) configured", len(m.status.Rules))
+	ruleCountLine := m.styles.Value.Render(ruleCountText)
+
+	content := lipgloss.JoinVertical(
+		lipgloss.Left,
+		title,
+		ruleCountLine,
+	)
+
+	return m.styles.SectionBorder.Render(content)
+}
+
+func (m model) getPolicyStyle(policy string) lipgloss.Style {
+	switch strings.ToUpper(policy) {
+	case "ALLOW":
+		return m.styles.AllowPolicy
+	case "DENY":
+		return m.styles.DenyPolicy
+	case "REJECT":
+		return m.styles.RejectPolicy
+	default:
+		return m.styles.Value
+	}
+}
+
+func (m model) renderFooter() string {
+	keys := []string{
+		m.styles.FooterKey.Render("r") + " refresh",
+		m.styles.FooterKey.Render("?") + " help",
+		m.styles.FooterKey.Render("q") + " quit",
+	}
+
+	footerText := strings.Join(keys, "  •  ")
+	return m.styles.Footer.Width(m.width).Render(footerText)
+}
internal/app/styles.go
@@ -0,0 +1,145 @@
+package app
+
+import (
+	"github.com/charmbracelet/lipgloss"
+)
+
+// Styles holds all the styling for the TUI
+type Styles struct {
+	// Title and header
+	Title    lipgloss.Style
+	Subtitle lipgloss.Style
+
+	// Status indicators
+	ActiveStatus   lipgloss.Style
+	InactiveStatus lipgloss.Style
+	StatusLabel    lipgloss.Style
+
+	// Sections
+	SectionBorder lipgloss.Style
+	SectionTitle  lipgloss.Style
+
+	// Policies
+	AllowPolicy  lipgloss.Style
+	DenyPolicy   lipgloss.Style
+	RejectPolicy lipgloss.Style
+
+	// Labels and values
+	Label lipgloss.Style
+	Value lipgloss.Style
+
+	// Footer
+	Footer    lipgloss.Style
+	FooterKey lipgloss.Style
+
+	// Error
+	Error lipgloss.Style
+}
+
+// NewStyles creates and returns styled components with Catppuccin Mocha theme
+func NewStyles() Styles {
+	// Catppuccin Mocha Color Palette
+	var (
+		// rosewater = lipgloss.Color("#f5e0dc")
+		// flamingo  = lipgloss.Color("#f2cdcd")
+		// pink      = lipgloss.Color("#f5c2e7")
+		mauve  = lipgloss.Color("#cba6f7")
+		red    = lipgloss.Color("#f38ba8")
+		maroon = lipgloss.Color("#eba0ac")
+		peach  = lipgloss.Color("#fab387")
+		// yellow    = lipgloss.Color("#f9e2af")
+		green = lipgloss.Color("#a6e3a1")
+		// teal      = lipgloss.Color("#94e2d5")
+		sky = lipgloss.Color("#89dceb")
+		// sapphire  = lipgloss.Color("#74c7ec")
+		// blue      = lipgloss.Color("#89b4fa")
+		lavender = lipgloss.Color("#b4befe")
+
+		text = lipgloss.Color("#cdd6f4")
+		// subtext1  = lipgloss.Color("#bac2de")
+		subtext0 = lipgloss.Color("#a6adc8")
+		overlay2 = lipgloss.Color("#9399b2")
+		overlay1 = lipgloss.Color("#7f849c")
+		// overlay0  = lipgloss.Color("#6c7086")
+		// surface2  = lipgloss.Color("#585b70")
+		// surface1  = lipgloss.Color("#45475a")
+		// surface0  = lipgloss.Color("#313244")
+		// base      = lipgloss.Color("#1e1e2e")
+		// mantle    = lipgloss.Color("#181825")
+		// crust     = lipgloss.Color("#11111b")
+	)
+
+	return Styles{
+		Title: lipgloss.NewStyle().
+			Bold(true).
+			Foreground(mauve).
+			MarginBottom(1).
+			Align(lipgloss.Center),
+
+		Subtitle: lipgloss.NewStyle().
+			Foreground(overlay2).
+			Italic(true).
+			Align(lipgloss.Center),
+
+		ActiveStatus: lipgloss.NewStyle().
+			Bold(true).
+			Foreground(green),
+
+		InactiveStatus: lipgloss.NewStyle().
+			Bold(true).
+			Foreground(red),
+
+		StatusLabel: lipgloss.NewStyle().
+			Foreground(text).
+			Bold(true),
+
+		SectionBorder: lipgloss.NewStyle().
+			Border(lipgloss.RoundedBorder()).
+			BorderForeground(mauve).
+			Padding(1, 2).
+			MarginBottom(1).Width(30),
+
+		SectionTitle: lipgloss.NewStyle().
+			Bold(true).
+			Foreground(lavender).
+			Underline(true).
+			MarginBottom(1),
+
+		AllowPolicy: lipgloss.NewStyle().
+			Bold(true).
+			Foreground(green),
+
+		DenyPolicy: lipgloss.NewStyle().
+			Bold(true).
+			Foreground(red),
+
+		RejectPolicy: lipgloss.NewStyle().
+			Bold(true).
+			Foreground(peach),
+
+		Label: lipgloss.NewStyle().
+			Foreground(subtext0).
+			Width(15),
+
+		Value: lipgloss.NewStyle().
+			Foreground(text).
+			Bold(true),
+
+		Footer: lipgloss.NewStyle().
+			Foreground(overlay1).
+			MarginTop(2).
+			Padding(1).
+			Align(lipgloss.Center),
+
+		FooterKey: lipgloss.NewStyle().
+			Foreground(sky).
+			Bold(true),
+
+		Error: lipgloss.NewStyle().
+			Foreground(red).
+			Bold(true).
+			Border(lipgloss.RoundedBorder()).
+			BorderForeground(maroon).
+			Padding(1, 2),
+	}
+}
internal/app/update.go
@@ -0,0 +1,32 @@
+package app
+
+import (
+	"ufWall/internal/ufw"
+
+	"github.com/charmbracelet/bubbles/key"
+	tea "github.com/charmbracelet/bubbletea"
+)
+
+func (m model) Init() tea.Cmd {
+	return nil
+}
+
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	switch msg := msg.(type) {
+	case tea.KeyMsg:
+		switch {
+		case key.Matches(msg, Keys.Quit):
+			return m, tea.Quit
+		case key.Matches(msg, Keys.Refresh):
+			m.status = ufw.GetStatus()
+			return m, nil
+		}
+
+	case tea.WindowSizeMsg:
+		m.width = msg.Width
+		m.height = msg.Height
+		return m, nil
+	}
+
+	return m, nil
+}
internal/app/view.go
@@ -0,0 +1,52 @@
+package app
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/charmbracelet/lipgloss"
+)
+
+func (m model) View() string {
+	var b strings.Builder
+
+	// // If there's an error, show it prominently
+	if m.status.Error != nil {
+		errorBox := m.styles.Error.Render(fmt.Sprintf("%v", m.status.Error))
+		footer := m.renderFooter()
+		return lipgloss.JoinVertical(
+			lipgloss.Center,
+			"",
+			errorBox,
+			"",
+			footer,
+		)
+	}
+
+	// Title
+	title := m.styles.Title.Width(m.width).Render("Firewall Manager")
+
+	b.WriteString(title)
+	b.WriteString("\n\n")
+
+	// Status Section
+	statusSection := m.renderStatusSection()
+	b.WriteString(statusSection)
+	b.WriteString("\n")
+
+	// Default Policies Section
+	policiesSection := m.renderPoliciesSection()
+	b.WriteString(policiesSection)
+	b.WriteString("\n")
+
+	// Rules Count Section
+	rulesSection := m.renderRulesSection()
+	b.WriteString(rulesSection)
+	b.WriteString("\n")
+
+	// Footer with keybindings
+	footer := m.renderFooter()
+	b.WriteString(footer)
+
+	return b.String()
+}
internal/ufw/main.go
@@ -0,0 +1,152 @@
+package ufw
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"os/exec"
+	"regexp"
+	"strings"
+)
+
+type UFWStatus struct {
+	Active        bool
+	DefaultIn     string
+	DefaultOut    string
+	DefaultRouted string
+	Logging       string
+	Rules         []Rule
+	RawVerbose    string
+	RawNumbered   string
+	Error         error
+}
+
+type Rule struct {
+	Num    int
+	To     string
+	Action string
+	From   string
+	Raw    string
+}
+
+var (
+	reStatus   = regexp.MustCompile(`(?i)Status:\s*(\w+)`)
+	reDefault  = regexp.MustCompile(`(?im)^Default:\s*(.+)$`)
+	reLogging  = regexp.MustCompile(`(?im)^Logging:\s*(.+)$`)
+	reRuleLine = regexp.MustCompile(`^\s*\[\s*(\d+)\]\s+(.+)$`)
+)
+
+func RunSudo(args ...string) (stdout, stderr string, err error) {
+	cmd := exec.Command("sudo", append([]string{"ufw"}, args...)...)
+	var out, errOut bytes.Buffer
+	cmd.Stdout = &out
+	cmd.Stderr = &errOut
+	err = cmd.Run()
+	return out.String(), errOut.String(), err
+}
+
+func GetStatus() UFWStatus {
+	var s UFWStatus
+	// Prefer numbered for rule list; use verbose for defaults/logging.
+	numOut, _, errNum := RunSudo("status", "numbered")
+	verbOut, _, errVerb := RunSudo("status", "verbose")
+	s.RawNumbered = numOut
+	s.RawVerbose = verbOut
+	if errNum != nil && errVerb != nil {
+		s.Error = fmt.Errorf("ufw status: %w", errNum)
+		return s
+	}
+
+	if m := reStatus.FindStringSubmatch(numOut); len(m) > 1 {
+		s.Active = strings.EqualFold(m[1], "active")
+	}
+	if m := reStatus.FindStringSubmatch(verbOut); len(m) > 1 {
+		s.Active = strings.EqualFold(m[1], "active")
+	}
+
+	if m := reDefault.FindStringSubmatch(verbOut); len(m) > 1 {
+		parts := strings.SplitSeq(strings.TrimSpace(m[1]), ",")
+		for p := range parts {
+			p = strings.TrimSpace(p)
+			lower := strings.ToLower(p)
+			if strings.Contains(lower, "incoming") {
+				s.DefaultIn = strings.TrimSuffix(p, "(incoming)")
+			}
+			if strings.Contains(lower, "outgoing") {
+				s.DefaultOut = strings.TrimSuffix(p, "(outgoing)")
+			}
+			if strings.Contains(lower, "routed") {
+				s.DefaultRouted = strings.TrimSuffix(p, "(routed)")
+			}
+		}
+	}
+	if m := reLogging.FindStringSubmatch(verbOut); len(m) > 1 {
+		s.Logging = m[1]
+	}
+
+	sc := bufio.NewScanner(strings.NewReader(numOut))
+	for sc.Scan() {
+		line := sc.Text()
+		if subm := reRuleLine.FindStringSubmatch(line); len(subm) >= 3 {
+			var num int
+			fmt.Sscanf(subm[1], "%d", &num)
+			rest := strings.TrimSpace(subm[2])
+			fields := splitRuleFields(rest)
+			r := Rule{Num: num, Raw: rest}
+			if len(fields) >= 3 {
+				r.To = fields[0]
+				r.Action = fields[1]
+				r.From = strings.Join(fields[2:], " ")
+			} else {
+				r.To = rest
+			}
+			s.Rules = append(s.Rules, r)
+		}
+	}
+	return s
+}
+
+func splitRuleFields(s string) []string {
+	var out []string
+	for f := range strings.FieldsSeq(s) {
+		out = append(out, f)
+	}
+	return out
+}
+
+func Enable() (stdout, stderr string, err error) {
+	return RunSudo("enable")
+}
+
+func Disable() (stdout, stderr string, err error) {
+	return RunSudo("disable")
+}
+
+func DeleteRule(num int) (stdout, stderr string, err error) {
+	return RunSudo("delete", fmt.Sprintf("%d", num))
+}
+
+func DefaultIncoming(allow bool) (stdout, stderr string, err error) {
+	pol := "deny"
+	if allow {
+		pol = "allow"
+	}
+	return RunSudo("default", pol, "incoming")
+}
+
+func DefaultOutgoing(allow bool) (stdout, stderr string, err error) {
+	pol := "deny"
+	if allow {
+		pol = "allow"
+	}
+	return RunSudo("default", pol, "outgoing")
+}
+
+// AddRule runs e.g. "sudo ufw allow 22/tcp".
+func AddRule(rule string) (stdout, stderr string, err error) {
+	parts := strings.Fields(rule)
+	if len(parts) == 0 {
+		return "", "", fmt.Errorf("empty rule")
+	}
+	return RunSudo(append([]string{}, parts...)...)
+}
.gitignore
@@ -0,0 +1,1 @@
+debug.log
go.mod
@@ -0,0 +1,32 @@
+module ufWall
+
+go 1.25.5
+
+require (
+	github.com/charmbracelet/bubbles v1.0.0
+	github.com/charmbracelet/bubbletea v1.3.10
+	github.com/charmbracelet/lipgloss v1.1.0
+)
+
+require (
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/charmbracelet/colorprofile v0.4.1 // indirect
+	github.com/charmbracelet/x/ansi v0.11.6 // indirect
+	github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
+	github.com/charmbracelet/x/term v0.2.2 // indirect
+	github.com/clipperhouse/displaywidth v0.9.0 // indirect
+	github.com/clipperhouse/stringish v0.1.1 // indirect
+	github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
+	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-localereader v0.0.1 // indirect
+	github.com/mattn/go-runewidth v0.0.19 // indirect
+	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+	github.com/muesli/cancelreader v0.2.2 // indirect
+	github.com/muesli/termenv v0.16.0 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+	golang.org/x/sys v0.41.0 // indirect
+	golang.org/x/text v0.3.8 // indirect
+)
go.sum
@@ -0,0 +1,50 @@
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
+github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
+github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
+github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
+github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
+github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
+github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
+github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
+github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
+github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
+github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
+github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
+github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
+github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
+github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
+github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
+github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=