Commit ffc94ce

Ansari <ping@ansari.wtf>
2026-02-26 19:32:11
revamped the folder structure - adapting nested models
1 parent b0fa5a8
internal/app/keys.go
@@ -3,25 +3,39 @@ package app
 import "github.com/charmbracelet/bubbles/key"
 
 type KeyMap struct {
-	Quit              key.Binding
-	Refresh           key.Binding
-	MoveToNextSection key.Binding
-	MoveToPrevSection key.Binding
+	Quit        key.Binding
+	Refresh     key.Binding
+	
+	NextSection key.Binding
+	PrevSection key.Binding
+	CursorUp    key.Binding
+	CursorDown  key.Binding
 }
 
 var Keys = KeyMap{
-	MoveToPrevSection: key.NewBinding(
+	CursorUp: key.NewBinding(
+		key.WithKeys("up"),
+	),
+
+	CursorDown: key.NewBinding(
+		key.WithKeys("down"),
+	),
+
+	PrevSection: key.NewBinding(
 		key.WithKeys("shift+tab", "left"),
 	),
-	MoveToNextSection: key.NewBinding(
+
+	NextSection: key.NewBinding(
 		key.WithKeys("tab", "right"),
 	),
-	Quit: key.NewBinding(
-		key.WithKeys("q", "ctrl+c", "esc"),
-		key.WithHelp("q/esc", "quit"),
-	),
+
 	Refresh: key.NewBinding(
 		key.WithKeys("r"),
 		key.WithHelp("r", "refresh"),
 	),
+
+	Quit: key.NewBinding(
+		key.WithKeys("q", "esc", "ctrl+c"),
+		key.WithHelp("q/esc", "quit"),
+	),
 }
internal/app/model.go
@@ -1,26 +1,44 @@
 package app
 
-import "ufWall/internal/ufw"
+import (
+	"ufWall/internal/sections/policy"
+	"ufWall/internal/sections/rules"
+	"ufWall/internal/sections/stats"
+	"ufWall/internal/ufw"
+	"ufWall/internal/ui"
+)
 
 type model struct {
-	status ufw.UFWStatus
-	rules  []ufw.Rule
+	stats ufw.Stats
+	policy ufw.Policy
+	rules []ufw.Rule
+  err error 
 
 	activeSection int
-	selectedRule int
 
-	styles Styles
 	width  int
 	height int
+	styles ui.Styles
+
+	statsSection stats.Model	
+	policySection policy.Model
+	rulesSection  rules.Model
 }
 
 func InitialModel() model {
-	s,r := ufw.GetStatus()
+	data := ufw.GetUFWData()
+	styles := ui.NewStyles()
 	return model{
-		status: s,
-		rules: r,
-		styles: NewStyles(),
-		width:   87,
-		height: 30,	
+		stats:  data.Stats,
+		rules:  data.Rules,
+		policy: data.Policy,
+
+		styles: styles,
+		width:  87,
+		height: 30,
+
+		statsSection: stats.New(styles),
+		policySection: policy.New(styles),
+		rulesSection: rules.New(styles),
 	}
 }
internal/app/sections.go
@@ -1,150 +0,0 @@
-package app
-
-import (
-	"fmt"
-	"strconv"
-	"strings"
-
-	"github.com/charmbracelet/lipgloss"
-)
-
-const (
-	SectionStatus = iota
-	SectionPolicies
-	SectionRules
-
-// SectionDetails
-)
-
-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)),
-	)
-
-	totalRulesLine := lipgloss.JoinHorizontal(
-		lipgloss.Left,
-		m.styles.Label.Render("Total Rules:"),
-		m.styles.Value.Render(strconv.Itoa(len(m.rules))),
-	)
-
-	content := lipgloss.JoinVertical(
-		lipgloss.Left,
-		statusLine,
-		loggingLine,
-		totalRulesLine,
-	)
-
-	return RenderBoxWithTitle("Firewall Stats", content, m.styles, -1, m.activeSection == SectionStatus)
-}
-
-func (m model) renderPoliciesSection() string {
-
-	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,
-		incomingLine,
-		outgoingLine,
-		routedLine,
-	)
-	return RenderBoxWithTitle("Default Policies", content, m.styles, -1, m.activeSection == SectionPolicies)
-}
-
-func (m model) renderRulesSection() string {
-	var rows []string
-
-	header := fmt.Sprintf(
-		"%-3s │ %-6s │ %-5s │ %-16s │ %-5s │ %-16s │ %-5s",
-		"#", "Action", "Proto", "Source", "sPort", "Destination", "dPort")
-
-	headerContent := m.styles.Label.UnsetWidth().Render(header)
-	line := strings.Repeat("─", lipgloss.Width(headerContent))
-	rows = append(rows, headerContent, m.styles.Label.UnsetWidth().Render(line))
-
-	for _, r := range m.rules {
-		row := fmt.Sprintf(
-			"%-3d │ %-6s │ %-5s │ %-16s │ %-5s │ %-16s │ %-5s",
-			r.Num,
-			r.Action,
-			r.ToProtocol,
-			r.FromSource,
-			r.FromPort,
-			r.ToDest,
-			r.ToPort,
-		)
-		rows = append(rows, m.styles.Value.Render(row))
-	}
-	table := strings.Join(rows, "\n")
-	content := lipgloss.JoinVertical(
-		lipgloss.Left,
-		table,
-	)
-	return RenderBoxWithTitle("Active Rules", content, m.styles, -1, m.activeSection == SectionRules)
-}
-
-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(width int) string {
-	var keys []string
-
-	switch m.activeSection {
-	case SectionRules:
-		keys = []string{"↑↓: navigate", "enter: details", "d: delete"}
-	default:
-		keys = []string{"tab: next section", "r: refresh"}
-	}
-
-	keys = append(keys, "q: quit")
-	footerText := strings.Join(keys, "  •  ")
-	return m.styles.Footer.Width(width).Render(footerText)
-}
internal/app/update.go
@@ -15,14 +15,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	case tea.KeyMsg:
 		switch {
-		case key.Matches(msg, Keys.MoveToNextSection): 
-			 m.activeSection = (m.activeSection + 1) % 3 
-		case key.Matches(msg, Keys.MoveToPrevSection):
-			 m.activeSection = (m.activeSection - 1 + 3) % 3 
+		case key.Matches(msg, Keys.NextSection):
+			m.activeSection = (m.activeSection + 1) % 3
+		case key.Matches(msg, Keys.PrevSection):
+			m.activeSection = (m.activeSection - 1 + 3) % 3
 		case key.Matches(msg, Keys.Quit):
 			return m, tea.Quit
 		case key.Matches(msg, Keys.Refresh):
-			m.status,m.rules = ufw.GetStatus()
+			data := ufw.GetUFWData()
+			m.rules = data.Rules
+			m.policy = data.Policy
+			m.stats = data.Stats
+			m.err = data.Error
 			return m, nil
 		}
 
internal/app/view.go
@@ -2,6 +2,8 @@ package app
 
 import (
 	"fmt"
+	"ufWall/internal/sections"
+	"ufWall/internal/ui"
 
 	"github.com/charmbracelet/lipgloss"
 )
@@ -19,12 +21,12 @@ func (m model) View() string {
 		)
 	}
 
-	if m.status.Error != nil {
-		footer := m.renderFooter(containerWidth)
+	if m.err != nil {
+		footer := ui.Footer(m.styles, m.activeSection, containerWidth)
 		return lipgloss.JoinVertical(
 			lipgloss.Center,
 			"",
-			m.styles.Error.Render(fmt.Sprintf("%v", m.status.Error)),
+			m.styles.Error.Render(fmt.Sprintf("%v", m.err)),
 			"",
 			footer,
 		)
@@ -36,14 +38,14 @@ func (m model) View() string {
 
 	infoSection := lipgloss.JoinHorizontal(
 		lipgloss.Top,
-		m.renderStatusSection(),
-		m.renderPoliciesSection(),
+		m.statsSection.View(m.stats,m.activeSection == sections.StatsSection),
+		m.policySection.View(m.policy,m.activeSection == sections.PolicySection),
 	)
 
 	content := lipgloss.JoinVertical(
 		lipgloss.Left,
 		infoSection,
-		m.renderRulesSection(),
+		m.rulesSection.View(m.rules,m.activeSection == sections.RulesSection),
 	)
 
 	layout := lipgloss.JoinVertical(
@@ -52,7 +54,7 @@ func (m model) View() string {
 		"",
 		content,
 		"",
-		m.renderFooter(containerWidth),
+		ui.Footer(m.styles, m.activeSection, containerWidth),
 	)
 
 	layout = lipgloss.Place(
internal/sections/policy/model.go
@@ -0,0 +1,15 @@
+package policy
+
+import (
+	"ufWall/internal/ui"
+)
+
+type Model struct {
+	styles ui.Styles
+}
+
+func New(styles ui.Styles) Model {
+	return Model{
+		styles,
+	}
+}
internal/sections/policy/view.go
@@ -0,0 +1,55 @@
+package policy
+
+import (
+	"strings"
+	"ufWall/internal/ufw"
+	"ufWall/internal/ui"
+
+	"github.com/charmbracelet/lipgloss"
+)
+
+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) View(policy ufw.Policy,active bool) string {
+
+	incomingStyle := m.getPolicyStyle(policy.DefaultIncoming)
+	outgoingStyle := m.getPolicyStyle(policy.DefaultOutgoing)
+	routedStyle := m.getPolicyStyle(policy.DefaultRouted)
+
+	incomingLine := lipgloss.JoinHorizontal(
+		lipgloss.Left,
+		m.styles.Label.Render("Incoming:"),
+		incomingStyle.Render(policy.DefaultIncoming),
+	)
+
+	outgoingLine := lipgloss.JoinHorizontal(
+		lipgloss.Left,
+		m.styles.Label.Render("Outgoing:"),
+		outgoingStyle.Render(policy.DefaultOutgoing),
+	)
+
+	routedLine := lipgloss.JoinHorizontal(
+		lipgloss.Left,
+		m.styles.Label.Render("Routed:"),
+		routedStyle.Render(policy.DefaultRouted),
+	)
+
+	content := lipgloss.JoinVertical(
+		lipgloss.Left,
+		incomingLine,
+		outgoingLine,
+		routedLine,
+	)
+	return ui.TitledBox("Default Policies", content, m.styles, -1, active)
+}
internal/sections/rules/model.go
@@ -0,0 +1,15 @@
+package rules
+
+import (
+	"ufWall/internal/ui"
+)
+
+type Model struct {
+	styles ui.Styles
+}
+
+func New(styles ui.Styles) Model {
+	return Model{
+		styles,
+	}
+}
internal/sections/rules/view.go
@@ -0,0 +1,42 @@
+package rules
+
+import (
+	"fmt"
+	"strings"
+	"ufWall/internal/ufw"
+	"ufWall/internal/ui"
+
+	"github.com/charmbracelet/lipgloss"
+)
+
+func (m Model) View(rules []ufw.Rule, active bool) string {
+	var rows []string
+
+	header := fmt.Sprintf(
+		"%-3s │ %-6s │ %-5s │ %-16s │ %-5s │ %-16s │ %-5s",
+		"#", "Action", "Proto", "Source", "sPort", "Destination", "dPort")
+
+	headerContent := m.styles.Label.UnsetWidth().Render(header)
+	line := strings.Repeat("─", lipgloss.Width(headerContent))
+	rows = append(rows, headerContent, m.styles.Label.UnsetWidth().Render(line))
+
+	for _, r := range rules {
+		row := fmt.Sprintf(
+			"%-3d │ %-6s │ %-5s │ %-16s │ %-5s │ %-16s │ %-5s",
+			r.Num,
+			r.Action,
+			r.ToProtocol,
+			r.FromSource,
+			r.FromPort,
+			r.ToDest,
+			r.ToPort,
+		)
+		rows = append(rows, m.styles.Value.Render(row))
+	}
+	table := strings.Join(rows, "\n")
+	content := lipgloss.JoinVertical(
+		lipgloss.Left,
+		table,
+	)
+	return ui.TitledBox("Active Rules", content, m.styles, -1, active)
+}
internal/sections/stats/model.go
@@ -0,0 +1,15 @@
+package stats
+
+import (
+	"ufWall/internal/ui"
+)
+
+type Model struct {
+	styles ui.Styles
+}
+
+func New(styles ui.Styles) Model {
+	return Model{
+		styles,
+	}
+}
internal/sections/stats/view.go
@@ -0,0 +1,50 @@
+package stats
+
+import (
+	"strconv"
+	"strings"
+	"ufWall/internal/ufw"
+	"ufWall/internal/ui"
+
+	"github.com/charmbracelet/lipgloss"
+)
+
+func (m Model) View(stats ufw.Stats, active bool) string {
+	var statusText string
+	var statusStyle lipgloss.Style
+
+	if stats.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(stats.Logging)),
+	)
+
+	totalRulesLine := lipgloss.JoinHorizontal(
+		lipgloss.Left,
+		m.styles.Label.Render("Total Rules:"),
+		m.styles.Value.Render(strconv.Itoa(stats.TotalRules)),
+	)
+
+	content := lipgloss.JoinVertical(
+		lipgloss.Left,
+		statusLine,
+		loggingLine,
+		totalRulesLine,
+	)
+
+	return ui.TitledBox("Firewall Stats", content, m.styles, -1, active)
+}
internal/sections/sections.go
@@ -0,0 +1,7 @@
+package sections
+
+const (
+	StatsSection = iota
+	PolicySection
+	RulesSection
+)
internal/ufw/main.go
@@ -8,16 +8,38 @@ import (
 	"strings"
 )
 
-type UFWStatus struct {
-	Active        bool
-	DefaultIn     string
-	DefaultOut    string
-	DefaultRouted string
-	Logging       string
-	RawVerbose    string
-	RawNumbered   string
-	Error         error
-	UpTime        string
+type Stats struct {
+	Active  bool
+	Logging string
+	TotalRules int
+}
+
+type Policy struct {
+	DefaultIncoming string
+	DefaultOutgoing string
+	DefaultRouted   string
+}
+
+type Rule struct {
+	Num    int
+	Action string
+
+	ToDest     string
+	ToPort     string
+	ToProtocol string
+
+	FromSource string
+	FromPort   string
+
+	Comment string
+	Raw     string
+}
+
+type ufwData struct {
+	Stats  Stats
+	Policy Policy
+	Rules  []Rule
+	Error  error
 }
 
 var (
@@ -48,23 +70,19 @@ func extractPolicy(s string) string {
 	return "unknown"
 }
 
-func GetStatus() (UFWStatus, []Rule) {
-	var s UFWStatus
+func GetUFWData() (ufwData) {
+	var data ufwData
 
 	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, nil
+		data.Error = fmt.Errorf("ufw status: %w", errNum)
+		return data
 	}
 
-	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")
+		data.Stats.Active = strings.EqualFold(m[1], "active")
 	}
 
 	if m := reDefault.FindStringSubmatch(verbOut); len(m) > 1 {
@@ -73,21 +91,24 @@ func GetStatus() (UFWStatus, []Rule) {
 			p = strings.TrimSpace(p)
 			lower := strings.ToLower(p)
 			if strings.Contains(lower, "incoming") {
-				s.DefaultIn = strings.ToUpper(extractPolicy(p))
+				data.Policy.DefaultIncoming = strings.ToUpper(extractPolicy(p))
 			}
 			if strings.Contains(lower, "outgoing") {
-				s.DefaultOut = strings.ToUpper(extractPolicy(p))
+				data.Policy.DefaultOutgoing = strings.ToUpper(extractPolicy(p))
 			}
 			if strings.Contains(lower, "routed") {
-				s.DefaultRouted = strings.ToUpper(extractPolicy(p))
+				data.Policy.DefaultRouted = strings.ToUpper(extractPolicy(p))
 			}
 		}
 	}
 	if m := reLogging.FindStringSubmatch(verbOut); len(m) > 1 {
-		s.Logging = m[1]
+		data.Stats.Logging = m[1]
 	}
-	rules := ParseRules(numOut)
-	return s, rules
+
+	data.Rules = ParseRules(numOut)
+	data.Stats.TotalRules = len(data.Rules)
+
+	return data
 }
 
 func Enable() (stdout, stderr string, err error) {
internal/ufw/rules.go
@@ -7,19 +7,6 @@ import (
 	"strings"
 )
 
-type Rule struct {
-	Num        int
-	Action     string
-	Direction  string
-	ToDest     string
-	ToPort     string
-	ToProtocol string
-	FromSource string
-	FromPort   string
-	Comment    string
-	Raw        string
-}
-
 func ParseRules(output string) []Rule {
 	var rules []Rule
 	reNumbered := regexp.MustCompile(`^\[\s*(\d+)\]\s+(.+)$`)
@@ -96,9 +83,6 @@ func parseRuleLine(num int, line string) Rule {
 	if len(actionParts) > 0 {
 		rule.Action = actionParts[0]
 	}
-	if len(actionParts) > 1 {
-		rule.Direction = actionParts[1]
-	}
 
 	if actionIndex > 0 {
 		toField := strings.Join(parts[:actionIndex], " ")
internal/ui/footer.go
@@ -0,0 +1,21 @@
+package ui
+
+import (
+	"strings"
+	"ufWall/internal/sections"
+)
+
+func Footer(styles Styles, activeSection int, width int) string {
+	var keys []string
+
+	switch activeSection {
+	case sections.RulesSection:
+		keys = []string{"↑↓: navigate", "enter: details", "d: delete"}
+	default:
+		keys = []string{"tab: next section", "r: refresh"}
+	}
+
+	keys = append(keys, "q: quit")
+	footerText := strings.Join(keys, "  •  ")
+	return styles.Footer.Width(width).Render(footerText)
+}
internal/app/styles.go → internal/ui/styles.go
@@ -1,39 +1,31 @@
-package app
+package ui
 
 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
 	SectionBorderActive 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
 }
 
@@ -62,7 +54,7 @@ func NewStyles() Styles {
 		overlay2 = lipgloss.Color("#9399b2")
 		overlay1 = lipgloss.Color("#7f849c")
 		// overlay0  = lipgloss.Color("#6c7086")
-		surface2  = lipgloss.Color("#585b70")
+		surface2 = lipgloss.Color("#585b70")
 		// surface1  = lipgloss.Color("#45475a")
 		// surface0  = lipgloss.Color("#313244")
 		// base      = lipgloss.Color("#1e1e2e")
internal/app/component.go → internal/ui/titled_box.go
@@ -1,4 +1,4 @@
-package app
+package ui
 
 import (
 	"strings"
@@ -6,12 +6,12 @@ import (
 	"github.com/charmbracelet/lipgloss"
 )
 
-func RenderBoxWithTitle(title, content string, styles Styles, width int, activeSession bool) string {
-	
+func TitledBox(title, content string, styles Styles, width int, activeSession bool) string {
+
 	borderColor := styles.SectionBorder.GetBorderTopForeground()
-	if activeSession{
+	if activeSession {
 		borderColor = styles.SectionBorderActive.GetBorderTopForeground()
-		title  = title + " ●"
+		title = title + " ●"
 	}
 
 	boxWidth := 0