Commit 95f99ae

Ansari <ping@ansari.wtf>
2026-02-28 21:53:20
Added overlay and stats section done
1 parent 7827549
internal/app/model.go
@@ -23,6 +23,8 @@ type model struct {
 	statsSection stats.Model	
 	policySection policy.Model
 	rulesSection  rules.Model
+
+	openMenu bool
 }
 
 func InitialModel() model {
@@ -40,5 +42,7 @@ func InitialModel() model {
 		statsSection: stats.New(styles),
 		policySection: policy.New(styles),
 		rulesSection: rules.New(styles),
+
+		openMenu : false,
 	}
 }
internal/app/overlay.go
@@ -0,0 +1,63 @@
+package app
+
+import (
+	"strings"
+
+	"github.com/charmbracelet/lipgloss"
+	"github.com/muesli/reflow/truncate"
+)
+
+func PlaceOverlay(x, y int, fg, bg string) string {
+	fgLines := strings.Split(fg, "\n")
+	bgLines := strings.Split(bg, "\n")
+	result := make([]string, len(bgLines))
+	copy(result, bgLines)
+
+	for i, fgLine := range fgLines {
+		bgY := y + i
+		if bgY < 0 || bgY >= len(bgLines) {
+			continue
+		}
+
+		fgW := uint(lipgloss.Width(fgLine))
+		bgLine := bgLines[bgY]
+		bgW := uint(lipgloss.Width(bgLine))
+
+		if uint(x) >= bgW {
+			continue
+		}
+
+		left := truncate.String(bgLine, uint(x))
+		right := ""
+		if end := uint(x) + fgW; end < bgW {
+			right = rightSlice(bgLine, int(end))
+		}
+
+		result[bgY] = left + fgLine + right
+	}
+
+	return strings.Join(result, "\n")
+}
+
+func rightSlice(s string, fromVisible int) string {
+	visible := 0
+	inEsc := false
+	runes := []rune(s)
+	for i, r := range runes {
+		if r == '\x1b' {
+			inEsc = true
+		}
+		if inEsc {
+			if r == 'm' {
+				inEsc = false
+			}
+			continue
+		}
+		if visible == fromVisible {
+			return string(runes[i:])
+		}
+		visible++
+	}
+	return ""
+}
+
internal/app/update.go
@@ -1,7 +1,7 @@
 package app
 
 import (
-	"log"
+	"ufWall/internal/keys"
 	"ufWall/internal/sections"
 	"ufWall/internal/ufw"
 
@@ -17,42 +17,42 @@ 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
+		if !m.isMenuOpen() {
+			switch {
+			case key.Matches(msg, keys.Bindings.NextSection):
+				m.blurAllSections()
+				m.activeSection = (m.activeSection + 1) % 3
+				m.focusActiveSection()
+				return m, nil
+
+			case key.Matches(msg, keys.Bindings.PrevSection):
+				m.blurAllSections()
+				m.activeSection = (m.activeSection - 1 + 3) % 3
+				m.focusActiveSection()
+				return m, nil
+
+			case key.Matches(msg, keys.Bindings.Refresh):
+				return m, keys.Refresh()
+
+			case key.Matches(msg, keys.Bindings.Quit):
+				return m, tea.Quit
 			}
 		}
 
-		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
+		switch m.activeSection {
+		case sections.StatsSection:
+			newStats, sectionCmd := m.statsSection.Update(msg)
+			m.statsSection = newStats
+			return m, sectionCmd
 		}
 
-	case RefreshMsg:
-		m.rules = msg.Rules
-		m.policy = msg.Policy
-		m.stats = msg.Stats
-		m.err = msg.Error
+	case keys.RefreshMsg:
+		data := ufw.GetUFWData()
+		m.rules = data.Rules
+		m.policy = data.Policy
+		m.stats = data.Stats
+		m.err = data.Error
 		return m, nil
 
 	case tea.WindowSizeMsg:
@@ -81,21 +81,6 @@ func (m *model) focusActiveSection() {
 	}
 }
 
-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
+func (m *model) isMenuOpen() bool {
+	return m.statsSection.GetMenu() != nil
 }
internal/app/view.go
@@ -66,25 +66,15 @@ func (m model) View() string {
 	)
 
 	if m.activeSection == sections.StatsSection && m.statsSection.GetMenu() != nil {
-		// Dim the background
-		dimmed := lipgloss.NewStyle().
-			Foreground(lipgloss.Color("#6c7086")). // Dimmed text
-			Render(layout)
+		dimmed := "\x1b[2m" + lipgloss.NewStyle().Faint(true).Render(layout) + "\x1b[0m"
+		menuView := m.statsSection.GetMenu().View(m.styles)
 
-		// Get menu
-		menuView := m.statsSection.GetMenu().View()
+		menuW := lipgloss.Width(menuView)
+		menuH := lipgloss.Height(menuView)
+		x := (m.width - menuW) / 2
+		y := (m.height - menuH) / 2
 
-		// 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 PlaceOverlay(x, y, menuView, dimmed)
 	}
 
 	return layout
internal/app/keys.go → internal/keys/keys.go
@@ -1,24 +1,28 @@
-package app
+package keys
 
-import "github.com/charmbracelet/bubbles/key"
+import (
+	"github.com/charmbracelet/bubbles/key"
+	tea "github.com/charmbracelet/bubbletea"
+)
 
 type KeyMap struct {
-	Quit        key.Binding
-	Refresh     key.Binding
-	
+	Quit    key.Binding
+	Refresh key.Binding
+
 	NextSection key.Binding
 	PrevSection key.Binding
 	CursorUp    key.Binding
 	CursorDown  key.Binding
+	Execute     key.Binding
 }
 
-var Keys = KeyMap{
+var Bindings = KeyMap{
 	CursorUp: key.NewBinding(
-		key.WithKeys("up"),
+		key.WithKeys("up", "k"),
 	),
 
 	CursorDown: key.NewBinding(
-		key.WithKeys("down"),
+		key.WithKeys("down", "j"),
 	),
 
 	PrevSection: key.NewBinding(
@@ -38,4 +42,16 @@ var Keys = KeyMap{
 		key.WithKeys("q", "esc", "ctrl+c"),
 		key.WithHelp("q/esc", "quit"),
 	),
+
+	Execute: key.NewBinding(
+		key.WithKeys("enter"),
+	),
+}
+
+type RefreshMsg struct{}
+
+func Refresh() tea.Cmd {
+	return func() tea.Msg {
+		return RefreshMsg{}
+	}
 }
internal/sections/stats/model.go
@@ -4,20 +4,12 @@ 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
-	cursorLine int // 0=status, 1=logging
+	totalOpts  int
+	cursorLine int 
 	showMenu   bool
 	menu       *ui.Menu
-	menuType   MenuType
 	active     bool
 }
 
@@ -27,8 +19,8 @@ func New(styles ui.Styles) Model {
 		cursorLine: 0,
 		showMenu:   false,
 		menu:       nil,
-		menuType:   MenuNone,
 		active:     true,
+		totalOpts:  2,
 	}
 }
 
@@ -48,6 +40,5 @@ func (m Model) HasOpenMenu() bool {
 }
 
 func (m Model) GetMenu() *ui.Menu {
-    return m.menu
+	return m.menu
 }
-
internal/sections/stats/update.go
@@ -1,99 +1,74 @@
 package stats
 
 import (
+	"log"
+	"ufWall/internal/keys"
+	"ufWall/internal/ufw"
 	"ufWall/internal/ui"
 
+	"github.com/charmbracelet/bubbles/key"
 	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)
+	if m.menu != nil {
+		if quit := m.menu.Update(msg); quit {
+			m.menu = nil
+			return m, keys.Refresh()
 		}
-		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--
+	} else {
+		switch msg := msg.(type) {
+		case tea.KeyMsg:
+			switch {
+			case key.Matches(msg, keys.Bindings.CursorUp):
+				if m.cursorLine > 0 {
+					m.cursorLine--
+				}
+			case key.Matches(msg, keys.Bindings.CursorDown):
+				if m.cursorLine < m.totalOpts - 1 {
+					m.cursorLine++
+				}
+			case key.Matches(msg, keys.Bindings.Execute):
+				return m.openMenu()
+			}
 		}
-
-	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
+	var options []ui.MenuItem
 
 	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"}
-	
+	case 0:
+		options = ui.MakeMenuItems(
+			[]string{"Enable Firewall", "Disable Firewall"},
+			func(label string) tea.Cmd {
+				switch label {
+				case "Enable Firewall":
+					log.Println("Enabling Firewall")
+					ufw.Enable()
+				case "Disable Firewall":
+					log.Println("Disabling Firewall")
+					ufw.Disable()
+					return nil
+				}
+				return nil
+			},
+		)
+
+	case 1:
+		options = ui.MakeMenuItems(
+			[]string{"off", "low", "medium", "high", "full"},
+			func(level string) tea.Cmd {
+				log.Println("Setting Log Level :", level)
+				ufw.SetLogging(level)
+				return nil
+			},
+		)
 	}
 
 	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
@@ -48,5 +48,5 @@ func (m Model) View(stats ufw.Stats) string {
 		ui.InsertCursor(rulesLine, false, m.styles),
 	)
 
-	return ui.TitledBox("Firewall Stats", content, m.styles, -1, m.active)
+	return ui.TitledBox("Firewall Stats", content, m.styles, 35, m.active)
 }
internal/ufw/main.go
@@ -139,6 +139,10 @@ func DefaultOutgoing(allow bool) (stdout, stderr string, err error) {
 	return RunSudo("default", pol, "outgoing")
 }
 
+func SetLogging(level string) (stdout, stderr string, err error) {
+	return RunSudo("logging", level)
+}
+
 func AddRule(rule string) (stdout, stderr string, err error) {
 	parts := strings.Fields(rule)
 	if len(parts) == 0 {
internal/ui/menu.go
@@ -3,17 +3,24 @@ package ui
 
 import (
 	"strings"
+	"ufWall/internal/keys"
 
-	"github.com/charmbracelet/lipgloss"
+	"github.com/charmbracelet/bubbles/key"
+	tea "github.com/charmbracelet/bubbletea"
 )
 
+type MenuItem struct {
+	Label  string
+	Action func() tea.Cmd
+}
+
 type Menu struct {
-	Options  []string
+	Options  []MenuItem
 	Selected int
 	styles   Styles
 }
 
-func NewMenu(options []string, styles Styles) Menu {
+func NewMenu(options []MenuItem, styles Styles) Menu {
 	return Menu{
 		Options:  options,
 		Selected: 0,
@@ -33,14 +40,16 @@ func (m *Menu) Down() {
 	}
 }
 
-func (m Menu) SelectedOption() string {
+func (m Menu) ExecuteSelected() tea.Cmd {
 	if m.Selected >= 0 && m.Selected < len(m.Options) {
-		return m.Options[m.Selected]
+		if m.Options[m.Selected].Action != nil {
+			return m.Options[m.Selected].Action()
+		}
 	}
-	return ""
+	return nil
 }
 
-func (m Menu) View() string {
+func (m Menu) View(styles Styles) string {
 	var lines []string
 
 	for i, option := range m.Options {
@@ -52,16 +61,45 @@ func (m Menu) View() string {
 			style = m.styles.ActiveStatus.Bold(true)
 		}
 
-		lines = append(lines, prefix+style.Render(option))
+		lines = append(lines, prefix+style.Render(option.Label))
 	}
 
 	content := strings.Join(lines, "\n")
 
-	menuStyle := lipgloss.NewStyle().
-		Border(lipgloss.RoundedBorder()).
-		BorderForeground(lipgloss.Color("#89b4fa")).
-		Padding(1, 2).
-		Background(lipgloss.Color("#1e1e2e"))
+	return styles.Menu.Render(content)
+}
+
+func (m *Menu) Update(msg tea.Msg) (bool) {
+	switch msg := msg.(type) {
+
+	case tea.KeyMsg:
+		switch {
+		case key.Matches(msg, keys.Bindings.CursorUp):
+			m.Up()
+		case key.Matches(msg, keys.Bindings.CursorDown):
+			m.Down()
+		case key.Matches(msg, keys.Bindings.Execute):
+			m.ExecuteSelected()
+			return true
+		case key.Matches(msg, keys.Bindings.Quit):
+			return true
+		}
+	}
+	return false
+}
+
+func MakeMenuItems(labels []string, handler func(string) tea.Cmd) []MenuItem {
+	items := make([]MenuItem, len(labels))
+
+	for i, label := range labels {
+		l := label // capture loop variable!
+		items[i] = MenuItem{
+			Label: l,
+			Action: func() tea.Cmd {
+				return handler(l)
+			},
+		}
+	}
 
-	return menuStyle.Render(content)
+	return items
 }
internal/ui/styles.go
@@ -16,6 +16,8 @@ type Styles struct {
 	SectionTitle        lipgloss.Style
 	SectionBorderActive lipgloss.Style
 
+	Menu lipgloss.Style
+
 	AllowPolicy  lipgloss.Style
 	DenyPolicy   lipgloss.Style
 	RejectPolicy lipgloss.Style
@@ -29,37 +31,22 @@ type Styles struct {
 	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")
+		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{
@@ -93,6 +80,11 @@ func NewStyles() Styles {
 			Border(lipgloss.RoundedBorder()).
 			BorderForeground(mauve),
 
+		Menu: lipgloss.NewStyle().
+			Border(lipgloss.RoundedBorder()).
+			BorderForeground(lipgloss.Color(yellow)).
+			Padding(1, 2),
+
 		SectionTitle: lipgloss.NewStyle().
 			Bold(true).
 			Foreground(lavender).