Commit ffc94ce
Changed files (17)
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/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