Commit 7827549
Changed files (12)
cmd/ufWall/main.go
@@ -10,6 +10,12 @@ import (
)
func main() {
+ f, err := tea.LogToFile("debug.log", "debug")
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+
p := tea.NewProgram(app.InitialModel(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Printf("Error: %v\n", err)
internal/app/update.go
@@ -1,6 +1,8 @@
package app
import (
+ "log"
+ "ufWall/internal/sections"
"ufWall/internal/ufw"
"github.com/charmbracelet/bubbles/key"
@@ -13,23 +15,46 @@ func (m model) Init() tea.Cmd {
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
+
case tea.KeyMsg:
+ switch m.activeSection {
+
+ case sections.StatsSection:
+ newStats, sectionCmd := m.statsSection.Update(msg)
+ m.statsSection = newStats
+ if m.statsSection.HasOpenMenu() {
+ return m, sectionCmd
+ }
+ }
+
switch {
case key.Matches(msg, Keys.NextSection):
+ m.blurAllSections()
m.activeSection = (m.activeSection + 1) % 3
+ m.focusActiveSection()
+ return m, nil
+
case key.Matches(msg, Keys.PrevSection):
+ m.blurAllSections()
m.activeSection = (m.activeSection - 1 + 3) % 3
+ m.focusActiveSection()
+ return m, nil
+
+ case key.Matches(msg, Keys.Refresh):
+ return m, m.refreshData()
+
case key.Matches(msg, Keys.Quit):
+ log.Println("EXITING")
return m, tea.Quit
- case key.Matches(msg, Keys.Refresh):
- data := ufw.GetUFWData()
- m.rules = data.Rules
- m.policy = data.Policy
- m.stats = data.Stats
- m.err = data.Error
- return m, nil
}
+ case RefreshMsg:
+ m.rules = msg.Rules
+ m.policy = msg.Policy
+ m.stats = msg.Stats
+ m.err = msg.Error
+ return m, nil
+
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
@@ -38,3 +63,39 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
+
+func (m *model) blurAllSections() {
+ m.statsSection.Blur()
+ m.policySection.Blur()
+ m.rulesSection.Blur()
+}
+
+func (m *model) focusActiveSection() {
+ switch m.activeSection {
+ case sections.StatsSection:
+ m.statsSection.Focus()
+ case sections.PolicySection:
+ m.policySection.Focus()
+ case sections.RulesSection:
+ m.rulesSection.Focus()
+ }
+}
+
+func (m model) refreshData() tea.Cmd {
+ return func() tea.Msg {
+ data := ufw.GetUFWData()
+ return RefreshMsg{
+ Rules: data.Rules,
+ Policy: data.Policy,
+ Stats: data.Stats,
+ Error: data.Error,
+ }
+ }
+}
+
+type RefreshMsg struct {
+ Rules []ufw.Rule
+ Policy ufw.Policy
+ Stats ufw.Stats
+ Error error
+}
internal/app/view.go
@@ -38,14 +38,14 @@ func (m model) View() string {
infoSection := lipgloss.JoinHorizontal(
lipgloss.Top,
- m.statsSection.View(m.stats,m.activeSection == sections.StatsSection),
- m.policySection.View(m.policy,m.activeSection == sections.PolicySection),
+ m.statsSection.View(m.stats),
+ m.policySection.View(m.policy),
)
content := lipgloss.JoinVertical(
lipgloss.Left,
infoSection,
- m.rulesSection.View(m.rules,m.activeSection == sections.RulesSection),
+ m.rulesSection.View(m.rules),
)
layout := lipgloss.JoinVertical(
@@ -65,5 +65,27 @@ func (m model) View() string {
layout,
)
+ if m.activeSection == sections.StatsSection && m.statsSection.GetMenu() != nil {
+ // Dim the background
+ dimmed := lipgloss.NewStyle().
+ Foreground(lipgloss.Color("#6c7086")). // Dimmed text
+ Render(layout)
+
+ // Get menu
+ menuView := m.statsSection.GetMenu().View()
+
+ // Center menu over dimmed background
+ centeredMenu := lipgloss.Place(
+ m.width,
+ m.height,
+ lipgloss.Center,
+ lipgloss.Center,
+ menuView,
+ )
+
+ // Use ANSI codes to position menu absolutely
+ return dimmed + layout + centeredMenu // Position at row 10, col 30
+ }
+
return layout
}
internal/sections/policy/model.go
@@ -6,10 +6,25 @@ import (
type Model struct {
styles ui.Styles
+ active bool
}
func New(styles ui.Styles) Model {
return Model{
- styles,
+ styles: styles,
+ active: false,
}
}
+
+func (m *Model) Focus() {
+ // m.cursorLine = 0
+ m.active = true
+}
+
+func (m *Model) Blur() {
+ m.active = false
+ // m.showMenu = false
+ // m.cursorLine = 0
+}
+
+
internal/sections/policy/view.go
@@ -21,7 +21,7 @@ func (m Model) getPolicyStyle(policy string) lipgloss.Style {
}
}
-func (m Model) View(policy ufw.Policy,active bool) string {
+func (m Model) View(policy ufw.Policy) string {
incomingStyle := m.getPolicyStyle(policy.DefaultIncoming)
outgoingStyle := m.getPolicyStyle(policy.DefaultOutgoing)
@@ -51,5 +51,5 @@ func (m Model) View(policy ufw.Policy,active bool) string {
outgoingLine,
routedLine,
)
- return ui.TitledBox("Default Policies", content, m.styles, -1, active)
+ return ui.TitledBox("Default Policies", content, m.styles, -1, m.active)
}
internal/sections/rules/model.go
@@ -6,10 +6,23 @@ import (
type Model struct {
styles ui.Styles
+ active bool
}
func New(styles ui.Styles) Model {
return Model{
- styles,
+ styles: styles,
+ active: false,
}
}
+
+func (m *Model) Focus() {
+ // m.cursorLine = 0
+ m.active = true
+}
+
+func (m *Model) Blur() {
+ m.active = false
+ // m.showMenu = false
+ // m.cursorLine = 0
+}
internal/sections/rules/view.go
@@ -9,7 +9,7 @@ import (
"github.com/charmbracelet/lipgloss"
)
-func (m Model) View(rules []ufw.Rule, active bool) string {
+func (m Model) View(rules []ufw.Rule) string {
var rows []string
header := fmt.Sprintf(
@@ -38,5 +38,5 @@ func (m Model) View(rules []ufw.Rule, active bool) string {
lipgloss.Left,
table,
)
- return ui.TitledBox("Active Rules", content, m.styles, -1, active)
+ return ui.TitledBox("Active Rules", content, m.styles, -1, m.active)
}
internal/sections/stats/model.go
@@ -4,12 +4,50 @@ import (
"ufWall/internal/ui"
)
+type MenuType int
+
+const (
+ MenuNone MenuType = iota
+ MenuFirewall // Enable/Disable
+ MenuLogging // off/on/low/medium/high/full
+)
+
type Model struct {
- styles ui.Styles
+ styles ui.Styles
+ cursorLine int // 0=status, 1=logging
+ showMenu bool
+ menu *ui.Menu
+ menuType MenuType
+ active bool
}
func New(styles ui.Styles) Model {
return Model{
- styles,
+ styles: styles,
+ cursorLine: 0,
+ showMenu: false,
+ menu: nil,
+ menuType: MenuNone,
+ active: true,
}
}
+
+func (m *Model) Focus() {
+ m.cursorLine = 0
+ m.active = true
+}
+
+func (m *Model) Blur() {
+ m.active = false
+ m.showMenu = false
+ m.cursorLine = 0
+}
+
+func (m Model) HasOpenMenu() bool {
+ return m.menu != nil
+}
+
+func (m Model) GetMenu() *ui.Menu {
+ return m.menu
+}
+
internal/sections/stats/update.go
@@ -0,0 +1,99 @@
+package stats
+
+import (
+ "ufWall/internal/ui"
+
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
+ if key, ok := msg.(tea.KeyMsg); ok {
+ if m.menu != nil {
+ return m.handleMenuInput(key)
+ }
+ return m.handleNavigation(key)
+ }
+ return m, nil
+}
+
+func (m Model) handleNavigation(key tea.KeyMsg) (Model, tea.Cmd) {
+ switch key.String() {
+ case "up", "k":
+ if m.cursorLine > 0 {
+ m.cursorLine--
+ }
+
+ case "down", "j":
+ if m.cursorLine < 1 {
+ m.cursorLine++
+ }
+
+ case "enter":
+ return m.openMenu()
+ }
+
+ return m, nil
+}
+
+func (m Model) openMenu() (Model, tea.Cmd) {
+ var options []string
+
+ switch m.cursorLine {
+ case 0:
+ m.menuType = MenuFirewall
+ options = []string{"Enable Firewall", "Disable Firewall"}
+
+ case 1:
+ m.menuType = MenuLogging
+ options = []string{"off", "low", "medium", "high", "full"}
+
+ }
+
+ menu := ui.NewMenu(options, m.styles)
+ m.menu = &menu
+ return m, nil
+}
+
+func (m Model) handleMenuInput(key tea.KeyMsg) (Model, tea.Cmd) {
+ switch key.String() {
+ case "up", "k":
+ m.menu.Up()
+
+ case "down", "j":
+ m.menu.Down()
+
+ case "enter":
+ return m.executeMenuAction()
+
+ case "esc":
+ m.menu = nil
+ m.menuType = MenuNone
+ }
+
+ return m, nil
+}
+
+func (m Model) executeMenuAction() (Model, tea.Cmd) {
+ if m.menu == nil {
+ return m, nil
+ }
+
+ // selectedOption := m.menu.SelectedOption()
+
+ switch m.menuType {
+ case MenuFirewall:
+ // Handle enable/disable
+ // TODO: Call UFW command based on selectedOption
+
+ case MenuLogging:
+ // Handle logging level change
+ // TODO: Call UFW command with selectedOption (off/on/low/etc)
+ }
+
+ // Close menu
+ m.menu = nil
+ m.menuType = MenuNone
+
+ return m, nil
+}
+
internal/sections/stats/view.go
@@ -9,7 +9,7 @@ import (
"github.com/charmbracelet/lipgloss"
)
-func (m Model) View(stats ufw.Stats, active bool) string {
+func (m Model) View(stats ufw.Stats) string {
var statusText string
var statusStyle lipgloss.Style
@@ -33,18 +33,20 @@ func (m Model) View(stats ufw.Stats, active bool) string {
m.styles.Value.Render(strings.ToUpper(stats.Logging)),
)
- totalRulesLine := lipgloss.JoinHorizontal(
+ rulesLine := lipgloss.JoinHorizontal(
lipgloss.Left,
m.styles.Label.Render("Total Rules:"),
m.styles.Value.Render(strconv.Itoa(stats.TotalRules)),
)
+ sectionActiveNoMenu := m.menu == nil && m.active
+
content := lipgloss.JoinVertical(
- lipgloss.Left,
- statusLine,
- loggingLine,
- totalRulesLine,
+ lipgloss.Top,
+ ui.InsertCursor(statusLine, m.cursorLine == 0 && sectionActiveNoMenu, m.styles),
+ ui.InsertCursor(loggingLine, m.cursorLine == 1 && sectionActiveNoMenu, m.styles),
+ ui.InsertCursor(rulesLine, false, m.styles),
)
- return ui.TitledBox("Firewall Stats", content, m.styles, -1, active)
+ return ui.TitledBox("Firewall Stats", content, m.styles, -1, m.active)
}
internal/ui/cursor.go
@@ -0,0 +1,12 @@
+package ui
+
+func InsertCursor(line string, selected bool, styles Styles) string {
+ prefix := " "
+ style := styles.Value
+ if selected {
+ prefix = "▶ "
+ style = styles.ActiveStatus.Bold(true)
+ }
+ line = prefix + style.Render(line)
+ return line
+}