Commit 95f99ae
Changed files (11)
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/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).