Commit a0ad4f8
internal/sections/rules/update.go
@@ -10,53 +10,504 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
-func (m Model) Update(msg tea.Msg, rules []ufw.Rule) (Model, tea.Cmd) {
+const (
+ ActionToggle = "toggle"
+ ActionMoveUp = "move_up"
+ ActionMoveDown = "move_down"
+ ActionAdd = "add"
+)
+
+func (m Model) Update(msg tea.Msg, data RulesData) (Model, tea.Cmd) {
+ rules := m.getActiveRules(data)
+
+ if m.addWizard != nil {
+ return m.updateAddWizard(msg)
+ }
+
+ if m.showDeleteConfirm {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "y", "Y":
+ if m.deleteRule != nil {
+ log.Printf("Deleting rule #%d (confirmed)", m.deleteRule.Num)
+ ufw.DeleteRule(m.deleteRule.Num)
+ }
+ m.showDeleteConfirm = false
+ m.deleteRule = nil
+ return m, keys.Refresh()
+ case "n", "N", "esc":
+ m.showDeleteConfirm = false
+ m.deleteRule = nil
+ return m, nil
+ }
+ }
+ return m, nil
+ }
+
+ if m.showDetails {
+ switch msg.(type) {
+ case tea.KeyMsg:
+ m.showDetails = false
+ m.detailRule = nil
+ return m, nil
+ }
+ return m, nil
+ }
+
if m.menu != nil {
if quit := m.menu.Update(msg); quit {
+ if m.menuContext != nil && m.menuContext.PendingSubmenu {
+ return m.handlePendingSubmenu()
+ }
m.menu = nil
+ m.menuContext = nil
return m, keys.Refresh()
}
- } else {
- switch msg := msg.(type) {
- case tea.KeyMsg:
+ return m, nil
+ }
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, keys.Bindings.SwitchTable):
+ if m.activeTable == IPv4Table {
+ m.activeTable = IPv6Table
+ } else {
+ m.activeTable = IPv4Table
+ }
+ return m, nil
+
+ case key.Matches(msg, keys.Bindings.CursorUp):
+ m.moveCursorUp()
+ return m, nil
+
+ case key.Matches(msg, keys.Bindings.CursorDown):
+ m.moveCursorDown(rules)
+ return m, nil
+
+ case key.Matches(msg, keys.Bindings.Execute):
+ if len(rules) > 0 {
+ return m.openMainMenu(data)
+ }
+ m.addWizard = NewAddWizard()
+ return m, nil
+
+ case key.Matches(msg, keys.Bindings.AddRule):
+ m.addWizard = NewAddWizard()
+ return m, nil
+
+ case key.Matches(msg, keys.Bindings.Info):
+ cursorLine := m.getCurrentCursor()
+ if len(rules) > 0 && cursorLine < len(rules) {
+ ruleCopy := rules[cursorLine]
+ m.showDetails = true
+ m.detailRule = &ruleCopy
+ return m, nil
+ }
+
+ case key.Matches(msg, keys.Bindings.Delete):
+ cursorLine := m.getCurrentCursor()
+ if len(rules) > 0 && cursorLine < len(rules) {
+ ruleCopy := rules[cursorLine]
+ m.showDeleteConfirm = true
+ m.deleteRule = &ruleCopy
+ return m, nil
+ }
+ }
+ }
+ return m, nil
+}
+
+func (m Model) updateAddWizard(msg tea.Msg) (Model, tea.Cmd) {
+ w := m.addWizard
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, keys.Bindings.Quit):
+ if w.InputMode {
+ w.InputMode = false
+ w.Input = ""
+ return m, nil
+ }
+ m.addWizard = nil
+ return m, nil
+ }
+
+ if w.InputMode {
switch {
- case key.Matches(msg, keys.Bindings.CursorUp):
- if m.cursorLine > 0 {
- m.cursorLine--
+ case key.Matches(msg, keys.Bindings.Execute):
+ return m.confirmWizardInput()
+ case key.Matches(msg, keys.Bindings.Back):
+ if len(w.Input) > 0 {
+ w.Input = w.Input[:len(w.Input)-1]
}
- case key.Matches(msg, keys.Bindings.CursorDown):
- if m.cursorLine < len(rules)-1 {
- m.cursorLine++
+ default:
+ if len(msg.String()) == 1 {
+ w.Input += msg.String()
}
- case key.Matches(msg, keys.Bindings.Execute):
- if len(rules) > 0 {
- return m.openMenu(rules)
+ }
+ return m, nil
+ }
+
+ switch {
+ case key.Matches(msg, keys.Bindings.CursorUp):
+ if w.Cursor > 0 {
+ w.Cursor--
+ }
+ case key.Matches(msg, keys.Bindings.CursorDown):
+ if w.Cursor < len(w.Options)-1 {
+ w.Cursor++
+ }
+ case key.Matches(msg, keys.Bindings.Execute):
+ return m.selectWizardOption()
+ case key.Matches(msg, keys.Bindings.CustomInput):
+ if w.Step == StepPort || w.Step == StepSource || w.Step == StepDestination {
+ w.InputMode = true
+ w.Input = ""
+ }
+ case key.Matches(msg, keys.Bindings.Back):
+ return m.wizardPrevStep()
+ }
+ }
+
+ return m, nil
+}
+
+func (m Model) selectWizardOption() (Model, tea.Cmd) {
+ w := m.addWizard
+ if w.Cursor >= len(w.Options) {
+ return m, nil
+ }
+
+ selected := w.Options[w.Cursor]
+
+ switch w.Step {
+ case StepAction:
+ w.Params.Action = selected
+ w.Step = StepDirection
+ w.Options = append([]string{"Both (in & out)"}, ufw.Directions...)
+ w.Cursor = 0
+
+ case StepDirection:
+ if selected == "Both (in & out)" {
+ w.Params.Direction = ""
+ } else {
+ w.Params.Direction = selected
+ }
+ w.Step = StepProtocol
+ w.Options = ufw.Protocols
+ w.Cursor = 0
+
+ case StepProtocol:
+ w.Params.Protocol = selected
+ w.Step = StepPort
+ w.Options = []string{"Any (no port filter)"}
+ for _, p := range ufw.CommonPorts {
+ w.Options = append(w.Options, p.Port+" ("+p.Name+")")
+ }
+ w.Cursor = 0
+
+ case StepPort:
+ if selected == "Any (no port filter)" {
+ w.Params.Port = ""
+ } else {
+ for i, c := range selected {
+ if c == ' ' {
+ w.Params.Port = selected[:i]
+ break
}
}
}
+ w.Step = StepSource
+ w.Options = []string{"Any", "Custom..."}
+ w.Cursor = 0
+
+ case StepSource:
+ switch selected {
+ case "Any":
+ w.Params.FromAddr = ""
+ case "Custom...":
+ w.InputMode = true
+ w.Input = ""
+ return m, nil
+ }
+ w.Step = StepDestination
+ w.Options = []string{"Any", "Custom..."}
+ w.Cursor = 0
+
+ case StepDestination:
+ switch selected {
+ case "Any":
+ w.Params.ToAddr = ""
+ case "Custom...":
+ w.InputMode = true
+ w.Input = ""
+ return m, nil
+ }
+ w.Step = StepInterface
+ w.Options = append([]string{"All interfaces"}, w.Interfaces...)
+ w.Cursor = 0
+
+ case StepInterface:
+ if selected == "All interfaces" {
+ w.Params.Interface = ""
+ } else {
+ w.Params.Interface = selected
+ }
+ w.Step = StepConfirm
+ w.Options = []string{"Confirm and Add Rule", "Cancel"}
+ w.Cursor = 0
+
+ case StepConfirm:
+ if selected == "Confirm and Add Rule" {
+ // Execute the command
+ log.Printf("Adding rule: %+v", w.Params)
+ _, stderr, err := ufw.AddNewRule(w.Params)
+ if err != nil {
+ log.Printf("Error adding rule: %s", stderr)
+ w.Error = stderr
+ return m, nil
+ }
+ m.addWizard = nil
+ return m, keys.Refresh()
+ } else {
+ // Cancel
+ m.addWizard = nil
+ return m, nil
+ }
}
+
return m, nil
}
-func (m Model) openMenu(rules []ufw.Rule) (Model, tea.Cmd) {
- if len(rules) == 0 || m.cursorLine >= len(rules) {
+func (m Model) confirmWizardInput() (Model, tea.Cmd) {
+ w := m.addWizard
+ input := w.Input
+ w.InputMode = false
+ w.Input = ""
+
+ switch w.Step {
+ case StepPort:
+ w.Params.Port = input
+ w.Step = StepSource
+ w.Options = []string{"Any", "Custom..."}
+ w.Cursor = 0
+
+ case StepSource:
+ w.Params.FromAddr = input
+ w.Step = StepDestination
+ w.Options = []string{"Any", "Custom..."}
+ w.Cursor = 0
+
+ case StepDestination:
+ w.Params.ToAddr = input
+ w.Step = StepInterface
+ w.Options = append([]string{"All interfaces"}, w.Interfaces...)
+ w.Cursor = 0
+ }
+
+ return m, nil
+}
+
+func (m Model) wizardPrevStep() (Model, tea.Cmd) {
+ w := m.addWizard
+
+ switch w.Step {
+ case StepAction:
+ m.addWizard = nil
+ return m, nil
+
+ case StepDirection:
+ w.Step = StepAction
+ w.Options = ufw.Actions
+ w.Cursor = 0
+
+ case StepProtocol:
+ w.Step = StepDirection
+ w.Options = append([]string{"Both (in & out)"}, ufw.Directions...)
+ w.Cursor = 0
+
+ case StepPort:
+ w.Step = StepProtocol
+ w.Options = ufw.Protocols
+ w.Cursor = 0
+
+ case StepSource:
+ w.Step = StepPort
+ w.Options = []string{"Any (no port filter)"}
+ for _, p := range ufw.CommonPorts {
+ w.Options = append(w.Options, p.Port+" ("+p.Name+")")
+ }
+ w.Cursor = 0
+
+ case StepDestination:
+ w.Step = StepSource
+ w.Options = []string{"Any", "Custom..."}
+ w.Cursor = 0
+
+ case StepInterface:
+ w.Step = StepDestination
+ w.Options = []string{"Any", "Custom..."}
+ w.Cursor = 0
+
+ case StepConfirm:
+ w.Step = StepInterface
+ w.Options = append([]string{"All interfaces"}, w.Interfaces...)
+ w.Cursor = 0
+ }
+
+ return m, nil
+}
+
+func (m Model) getActiveRules(data RulesData) []ufw.Rule {
+ if m.activeTable == IPv6Table {
+ return data.IPv6
+ }
+ return data.IPv4
+}
+
+func (m Model) getCurrentCursor() int {
+ if m.activeTable == IPv6Table {
+ return m.ipv6CursorLine
+ }
+ return m.ipv4CursorLine
+}
+
+func (m *Model) moveCursorUp() {
+ if m.activeTable == IPv6Table {
+ if m.ipv6CursorLine > 0 {
+ m.ipv6CursorLine--
+ }
+ } else {
+ if m.ipv4CursorLine > 0 {
+ m.ipv4CursorLine--
+ }
+ }
+}
+
+func (m *Model) moveCursorDown(rules []ufw.Rule) {
+ if m.activeTable == IPv6Table {
+ if m.ipv6CursorLine < len(rules)-1 {
+ m.ipv6CursorLine++
+ }
+ } else {
+ if m.ipv4CursorLine < len(rules)-1 {
+ m.ipv4CursorLine++
+ }
+ }
+}
+
+func (m Model) handlePendingSubmenu() (Model, tea.Cmd) {
+ if m.menuContext == nil {
+ m.menu = nil
+ return m, keys.Refresh()
+ }
+
+ log.Printf("handlePendingSubmenu: Action=%s, PendingSubmenu=%v", m.menuContext.Action, m.menuContext.PendingSubmenu)
+ m.menuContext.PendingSubmenu = false
+
+ switch m.menuContext.Action {
+ case ActionToggle:
+ return m.openToggleActionMenu()
+
+ case ActionMoveUp:
+ rule := m.menuContext.SelectedRule
+ if rule != nil {
+ log.Printf("Moving rule #%d up (IPv6=%v)", rule.Num, rule.IPv6)
+ err := ufw.MoveRule(*rule, -1, rule.Action)
+ if err != nil {
+ log.Printf("Error moving rule up: %v", err)
+ }
+ }
+ m.menu = nil
+ m.menuContext = nil
+ return m, keys.Refresh()
+
+ case ActionMoveDown:
+ rule := m.menuContext.SelectedRule
+ if rule != nil {
+ log.Printf("Moving rule #%d down (IPv6=%v)", rule.Num, rule.IPv6)
+ err := ufw.MoveRule(*rule, 1, rule.Action)
+ if err != nil {
+ log.Printf("Error moving rule down: %v", err)
+ }
+ }
+ m.menu = nil
+ m.menuContext = nil
+ return m, keys.Refresh()
+
+ case ActionAdd:
+ m.menu = nil
+ m.menuContext = nil
+ m.addWizard = NewAddWizard()
return m, nil
}
- selectedRule := rules[m.cursorLine]
- log.Println(selectedRule)
+ m.menu = nil
+ m.menuContext = nil
+ return m, keys.Refresh()
+}
+
+func (m Model) openMainMenu(data RulesData) (Model, tea.Cmd) {
+ rules := m.getActiveRules(data)
+ cursorLine := m.getCurrentCursor()
- options := ui.MakeMenuItems(
- []string{"Delete Rule"},
- func(label string) tea.Cmd {
- switch label {
- case "Delete Rule":
- log.Printf("Deleting rule #%d", selectedRule.Num)
- ufw.DeleteRule(selectedRule.Num)
+ if len(rules) == 0 || cursorLine >= len(rules) {
+ return m, nil
+ }
+
+ selectedRule := rules[cursorLine]
+ ruleCopy := selectedRule // Make a copy to store in context
+
+ menuLabels := []string{
+ "Toggle Action",
+ "Move Up",
+ "Move Down",
+ "Add New Rule",
+ }
+
+ m.menuContext = &MenuContext{
+ SelectedRule: &ruleCopy,
+ TotalRules: len(rules),
+ }
+
+ options := ui.MakeMenuItems(menuLabels, func(label string) tea.Cmd {
+ switch label {
+ case "Toggle Action":
+ m.menuContext.Action = ActionToggle
+ case "Move Up":
+ m.menuContext.Action = ActionMoveUp
+ case "Move Down":
+ m.menuContext.Action = ActionMoveDown
+ case "Add New Rule":
+ m.menuContext.Action = ActionAdd
+ }
+ m.menuContext.PendingSubmenu = true
+ return nil
+ })
+
+ menu := ui.NewMenu(options, m.styles)
+ m.menu = &menu
+ return m, nil
+}
+
+func (m Model) openToggleActionMenu() (Model, tea.Cmd) {
+ rule := m.menuContext.SelectedRule
+
+ options := ui.MakeMenuItems(ufw.Actions, func(newAction string) tea.Cmd {
+ if rule != nil && newAction != rule.Action {
+ log.Printf("Toggling rule #%d from %s to %s (IPv6=%v)", rule.Num, rule.Action, newAction, rule.IPv6)
+
+ err := ufw.MoveRule(*rule, 0, newAction)
+ if err != nil {
+ log.Printf("Error toggling rule: %v", err)
}
- return nil
- },
- )
+ }
+ m.menuContext = nil
+ return nil
+ })
menu := ui.NewMenu(options, m.styles)
m.menu = &menu
internal/sections/rules/view.go
@@ -10,9 +10,63 @@ import (
"github.com/charmbracelet/lipgloss"
)
-func (m Model) View(rules []ufw.Rule) string {
+type RulesData struct {
+ IPv4 []ufw.Rule
+ IPv6 []ufw.Rule
+}
+
+func (m Model) View(data RulesData) string {
+ sectionActiveNoMenu := m.menu == nil && m.active && !m.showDetails && !m.showDeleteConfirm
+
+ ipTable := m.renderTable(data.IPv4, data.IPv6, m.ipv4CursorLine, sectionActiveNoMenu && m.activeTable == IPv4Table)
+ if m.activeTable == IPv6Table {
+ ipTable = m.renderTable(data.IPv4, data.IPv6, m.ipv6CursorLine, sectionActiveNoMenu && m.activeTable == IPv6Table)
+ }
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ ipTable,
+ )
+
+ return ui.TitledBox("Active Rules", content, m.styles, -1, m.active)
+}
+
+func (m Model) renderTable(ipV4Rules []ufw.Rule, ipV6Rules []ufw.Rule, cursorLine int, isActive bool) string {
var rows []string
+ titleStyle := m.styles.Label
+ if isActive {
+ titleStyle = titleStyle.Bold(true).Foreground(lipgloss.Color("12"))
+ }
+ activeTitle := "IPv4 Rules"
+ inActiveTitle := "IPv6 Rules"
+
+ rules := ipV4Rules
+
+ maxIPv4 := len(fmt.Sprintf("%s (%d)", "IPv4 Rules", 9999))
+ maxIPv6 := len(fmt.Sprintf("%s (%d)", "IPv6 Rules", 9999))
+
+ padTitle := func(title string, count int, width int) string {
+ return fmt.Sprintf("%-*s", width, fmt.Sprintf("%s (%d)", title, count))
+ }
+
+ tabTitle := titleStyle.Render(padTitle(activeTitle, len(ipV4Rules), maxIPv4)) + "| " +
+ m.styles.Label.Foreground(lipgloss.Color("241")).Render(padTitle(inActiveTitle, len(ipV6Rules), maxIPv6))
+
+ if m.activeTable == IPv6Table {
+ rules = ipV6Rules
+ tabTitle = m.styles.Label.Foreground(lipgloss.Color("241")).Render(padTitle(activeTitle, len(ipV4Rules), maxIPv4)) + "| " +
+ titleStyle.Render(padTitle(inActiveTitle, len(ipV6Rules), maxIPv6))
+ }
+
+ rows = append(rows, tabTitle)
+ rows = append(rows, "")
+
+ if len(rules) == 0 {
+ emptyMsg := m.styles.Label.Foreground(lipgloss.Color("241")).Render(" (no rules)")
+ rows = append(rows, emptyMsg)
+ return strings.Join(rows, "\n")
+ }
+
header := fmt.Sprintf(
"%-3s │ %-6s │ %-5s │ %-16s │ %-5s │ %-16s │ %-5s",
"#", "Action", "Proto", "Source", "sPort", "Destination", "dPort")
@@ -21,8 +75,6 @@ func (m Model) View(rules []ufw.Rule) string {
line := strings.Repeat("─", lipgloss.Width(headerContent))
rows = append(rows, " "+headerContent, " "+m.styles.Label.UnsetWidth().Render(line))
- sectionActiveNoMenu := m.menu == nil && m.active && !m.showDetails && !m.showDeleteConfirm
-
for i, r := range rules {
action := fmt.Sprintf("%-6s", r.Action)
row := fmt.Sprintf(
@@ -30,22 +82,25 @@ func (m Model) View(rules []ufw.Rule) string {
r.Num,
action,
r.ToProtocol,
- r.FromSource,
- r.FromPort,
- r.ToDest,
- r.ToPort,
+ truncate(r.FromSource, 16),
+ truncate(r.FromPort, 5),
+ truncate(r.ToDest, 16),
+ truncate(r.ToPort, 5),
)
- rows = append(rows, ui.InsertCursorRulesSection(row, m.cursorLine == i && sectionActiveNoMenu, m.styles, r.Action))
+ rows = append(rows, ui.InsertCursorRulesSection(row, cursorLine == i && isActive, m.styles, r.Action))
}
- table := strings.Join(rows, "\n")
- content := lipgloss.JoinVertical(
- lipgloss.Left,
- table,
- )
- return ui.TitledBox("Active Rules", content, m.styles, -1, m.active)
+
+ return strings.Join(rows, "\n")
+}
+
+func truncate(s string, maxLen int) string {
+ s = strings.TrimSuffix(s, " (v6)")
+ if len(s) > maxLen {
+ return s[:maxLen-1] + "…"
+ }
+ return s
}
-// DetailView renders the rule detail overlay
func (m Model) DetailView() string {
if m.detailRule == nil {
return ""
@@ -53,12 +108,18 @@ func (m Model) DetailView() string {
r := m.detailRule
- // Build detail content
var lines []string
lines = append(lines, "")
lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Rule #:"), m.styles.Value.Render(fmt.Sprintf("%d", r.Num))))
lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Action:"), ui.GetPolicyStyle(m.styles, r.Action).Render(r.Action)))
lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Protocol:"), m.styles.Value.Render(r.ToProtocol)))
+
+ ipVersion := "IPv4"
+ if r.IPv6 {
+ ipVersion = "IPv6"
+ }
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("IP Version:"), m.styles.Value.Render(ipVersion)))
+
lines = append(lines, "")
lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Source:"), m.styles.Value.Render(r.FromSource)))
lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Source Port:"), m.styles.Value.Render(r.FromPort)))
@@ -136,3 +197,130 @@ func (m Model) DeleteConfirmView() string {
return ui.TitledBox("Confirm Delete", content, m.styles, -1, true)
}
+
+func (m Model) AddWizardView() string {
+ w := m.addWizard
+ if w == nil {
+ return ""
+ }
+
+ var lines []string
+ lines = append(lines, "")
+
+ if w.Params.Action != "" {
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Action:"), ui.GetPolicyStyle(m.styles, w.Params.Action).Render(w.Params.Action)))
+ }
+ if w.Step > StepDirection && w.Params.Direction != "" {
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Direction:"), m.styles.Value.Render(w.Params.Direction)))
+ } else if w.Step > StepDirection {
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Direction:"), m.styles.Value.Render("both")))
+ }
+ if w.Step > StepProtocol {
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Protocol:"), m.styles.Value.Render(w.Params.Protocol)))
+ }
+ if w.Step > StepPort {
+ port := w.Params.Port
+ if port == "" {
+ port = "any"
+ }
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Port:"), m.styles.Value.Render(port)))
+ }
+ if w.Step > StepSource {
+ src := w.Params.FromAddr
+ if src == "" {
+ src = "any"
+ }
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Source:"), m.styles.Value.Render(src)))
+ }
+ if w.Step > StepDestination {
+ dst := w.Params.ToAddr
+ if dst == "" {
+ dst = "any"
+ }
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Destination:"), m.styles.Value.Render(dst)))
+ }
+ if w.Step > StepInterface {
+ iface := w.Params.Interface
+ if iface == "" {
+ iface = "all"
+ }
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Interface:"), m.styles.Value.Render(iface)))
+ }
+
+ if len(lines) > 1 {
+ lines = append(lines, "")
+ }
+
+ stepTitle := m.getWizardStepTitle(w.Step)
+ lines = append(lines, m.styles.Title.Render(fmt.Sprintf(" %s", stepTitle)))
+ lines = append(lines, "")
+
+ if w.Error != "" {
+ lines = append(lines, m.styles.Error.Render(fmt.Sprintf(" Error: %s", w.Error)))
+ lines = append(lines, "")
+ }
+
+ if w.InputMode {
+ inputBox := fmt.Sprintf(" > %s_", w.Input)
+ lines = append(lines, m.styles.Value.Render(inputBox))
+ lines = append(lines, "")
+ lines = append(lines, m.styles.Label.Foreground(lipgloss.Color("241")).Render(" [Enter] Confirm [Esc] Cancel"))
+ } else {
+ for i, opt := range w.Options {
+ prefix := " "
+ style := m.styles.Label
+ if i == w.Cursor {
+ prefix = "> "
+ style = m.styles.Value.Bold(true)
+ }
+ lines = append(lines, style.Render(fmt.Sprintf(" %s%s", prefix, opt)))
+ }
+ lines = append(lines, "")
+
+ hints := "[Enter] Select [Esc] Cancel"
+ if w.Step > StepAction {
+ hints = "[b] Back [Esc] Cancel"
+ }
+ if w.Step == StepPort || w.Step == StepSource || w.Step == StepDestination {
+ hints = "[c] Custom [b] Back [Esc] Cancel"
+ }
+ lines = append(lines, m.styles.Label.UnsetWidth().Foreground(lipgloss.Color("241")).Render(hints))
+ }
+
+ lines = append(lines, "")
+
+ // Show preview command at confirm step
+ // if w.Step == StepConfirm {
+ // args := ufw.BuildAddRuleCommand(w.Params)
+ // cmdPreview := "sudo ufw " + strings.Join(args, " ")
+ // lines = append(lines, m.styles.Label.Render(" Command:"))
+ // lines = append(lines, m.styles.Value.Render(fmt.Sprintf(" %s", cmdPreview)))
+ // lines = append(lines, "")
+ // }
+
+ content := strings.Join(lines, "\n")
+ return ui.TitledBox("Add New Rule", content, m.styles, 40, true)
+}
+
+func (m Model) getWizardStepTitle(step WizardStep) string {
+ switch step {
+ case StepAction:
+ return "Select Action"
+ case StepDirection:
+ return "Select Direction"
+ case StepProtocol:
+ return "Select Protocol"
+ case StepPort:
+ return "Select Port"
+ case StepSource:
+ return "Select Source Address"
+ case StepDestination:
+ return "Select Destination Address"
+ case StepInterface:
+ return "Select Interface"
+ case StepConfirm:
+ return "Confirm Rule"
+ default:
+ return "Add Rule"
+ }
+}
internal/ufw/rules.go
@@ -62,6 +62,8 @@ func parseRuleLine(num int, line string) Rule {
Raw: line,
}
+ rule.IPv6 = strings.Contains(line, "(v6)")
+
commentIdx := strings.Index(line, "#")
if commentIdx >= 0 {
rule.Comment = strings.TrimSpace(line[commentIdx+1:])