Commit 2303e0b
Changed files (4)
internal
internal/sections/rules/model.go
@@ -51,8 +51,12 @@ type Model struct {
// Two-table navigation
activeTable Table // Which table (IPv4 or IPv6) is currently focused
- ipv4CursorLine int
- ipv6CursorLine int
+ ipv4CursorLine int
+ ipv6CursorLine int
+
+ // Scroll offsets for each table
+ ipv4ScrollOffset int
+ ipv6ScrollOffset int
// Detail view overlay
showDetails bool
@@ -64,9 +68,11 @@ type Model struct {
// Multi-step menu operations
menuContext *MenuContext
- addWizard *AddWizard
+ addWizard *AddWizard
}
+const MaxVisibleRules = 7
+
func New(styles ui.Styles) Model {
return Model{
styles: styles,
@@ -76,6 +82,8 @@ func New(styles ui.Styles) Model {
activeTable: IPv4Table,
ipv4CursorLine: 0,
ipv6CursorLine: 0,
+ ipv4ScrollOffset: 0,
+ ipv6ScrollOffset: 0,
showDetails: false,
detailRule: nil,
showDeleteConfirm: false,
@@ -141,6 +149,13 @@ func (m Model) CurrentCursorLine() int {
return m.ipv4CursorLine
}
+func (m Model) CurrentScrollOffset() int {
+ if m.activeTable == IPv6Table {
+ return m.ipv6ScrollOffset
+ }
+ return m.ipv4ScrollOffset
+}
+
func (m Model) ShowingAddWizard() bool {
return m.addWizard != nil
}
internal/sections/rules/update.go
@@ -27,8 +27,8 @@ func (m Model) Update(msg tea.Msg, data RulesData) (Model, tea.Cmd) {
if m.showDeleteConfirm {
switch msg := msg.(type) {
case tea.KeyMsg:
- switch msg.String() {
- case "y", "Y":
+ switch {
+ case key.Matches(msg, keys.Bindings.Execute):
if m.deleteRule != nil {
log.Printf("Deleting rule #%d (confirmed)", m.deleteRule.Num)
ufw.DeleteRule(m.deleteRule.Num)
@@ -36,7 +36,7 @@ func (m Model) Update(msg tea.Msg, data RulesData) (Model, tea.Cmd) {
m.showDeleteConfirm = false
m.deleteRule = nil
return m, keys.Refresh()
- case "n", "N", "esc":
+ case key.Matches(msg, keys.Bindings.Quit):
m.showDeleteConfirm = false
m.deleteRule = nil
return m, nil
@@ -379,10 +379,18 @@ func (m *Model) moveCursorUp() {
if m.activeTable == IPv6Table {
if m.ipv6CursorLine > 0 {
m.ipv6CursorLine--
+ // Scroll up if cursor goes above visible area
+ if m.ipv6CursorLine < m.ipv6ScrollOffset {
+ m.ipv6ScrollOffset = m.ipv6CursorLine
+ }
}
} else {
if m.ipv4CursorLine > 0 {
m.ipv4CursorLine--
+ // Scroll up if cursor goes above visible area
+ if m.ipv4CursorLine < m.ipv4ScrollOffset {
+ m.ipv4ScrollOffset = m.ipv4CursorLine
+ }
}
}
}
@@ -391,10 +399,18 @@ func (m *Model) moveCursorDown(rules []ufw.Rule) {
if m.activeTable == IPv6Table {
if m.ipv6CursorLine < len(rules)-1 {
m.ipv6CursorLine++
+ // Scroll down if cursor goes below visible area
+ if m.ipv6CursorLine >= m.ipv6ScrollOffset+MaxVisibleRules {
+ m.ipv6ScrollOffset = m.ipv6CursorLine - MaxVisibleRules + 1
+ }
}
} else {
if m.ipv4CursorLine < len(rules)-1 {
m.ipv4CursorLine++
+ // Scroll down if cursor goes below visible area
+ if m.ipv4CursorLine >= m.ipv4ScrollOffset+MaxVisibleRules {
+ m.ipv4ScrollOffset = m.ipv4CursorLine - MaxVisibleRules + 1
+ }
}
}
}
internal/sections/rules/view.go
@@ -18,29 +18,30 @@ type RulesData struct {
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)
- }
+ ipTable := m.renderTable(data.IPv4, data.IPv6, sectionActiveNoMenu)
+
content := lipgloss.JoinVertical(
lipgloss.Left,
ipTable,
)
- return ui.TitledBox("Active Rules", content, m.styles, -1, m.active)
+ return ui.TitledBox("Active Rules", content, m.styles, -1, m.active, 13)
}
-func (m Model) renderTable(ipV4Rules []ufw.Rule, ipV6Rules []ufw.Rule, cursorLine int, isActive bool) string {
+func (m Model) renderTable(ipV4Rules []ufw.Rule, ipV6Rules []ufw.Rule, 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
+ scrollOffset := m.ipv4ScrollOffset
+ cursorLine := m.ipv4CursorLine
maxIPv4 := len(fmt.Sprintf("%s (%d)", "IPv4 Rules", 9999))
maxIPv6 := len(fmt.Sprintf("%s (%d)", "IPv6 Rules", 9999))
@@ -53,11 +54,20 @@ func (m Model) renderTable(ipV4Rules []ufw.Rule, ipV6Rules []ufw.Rule, cursorLin
m.styles.Label.Foreground(lipgloss.Color("241")).Render(padTitle(inActiveTitle, len(ipV6Rules), maxIPv6))
if m.activeTable == IPv6Table {
+ scrollOffset = m.ipv6ScrollOffset
+ cursorLine = m.ipv6CursorLine
rules = ipV6Rules
tabTitle = m.styles.Label.Foreground(lipgloss.Color("241")).Render(padTitle(activeTitle, len(ipV4Rules), maxIPv4)) + "| " +
titleStyle.Render(padTitle(inActiveTitle, len(ipV6Rules), maxIPv6))
}
+ if len(rules) > MaxVisibleRules {
+ scrollInfo := m.renderScrollIndicator(len(rules), scrollOffset)
+ lineWidth := lipgloss.Width(tabTitle)
+ scrollInfoRight := lipgloss.PlaceHorizontal(lineWidth, lipgloss.Right, scrollInfo)
+ tabTitle = lipgloss.JoinHorizontal(lipgloss.Left, tabTitle, scrollInfoRight)
+ }
+
rows = append(rows, tabTitle)
rows = append(rows, "")
@@ -75,7 +85,11 @@ func (m Model) renderTable(ipV4Rules []ufw.Rule, ipV6Rules []ufw.Rule, cursorLin
line := strings.Repeat("─", lipgloss.Width(headerContent))
rows = append(rows, " "+headerContent, " "+m.styles.Label.UnsetWidth().Render(line))
- for i, r := range rules {
+ startIdx := scrollOffset
+ endIdx := min(scrollOffset+MaxVisibleRules, len(rules))
+
+ for i := startIdx; i < endIdx; i++ {
+ r := rules[i]
action := fmt.Sprintf("%-6s", r.Action)
row := fmt.Sprintf(
"%-3d │ %6s │ %-5s │ %-16s │ %-5s │ %-16s │ %-5s",
@@ -89,10 +103,16 @@ func (m Model) renderTable(ipV4Rules []ufw.Rule, ipV6Rules []ufw.Rule, cursorLin
)
rows = append(rows, ui.InsertCursorRulesSection(row, cursorLine == i && isActive, m.styles, r.Action))
}
-
return strings.Join(rows, "\n")
}
+func (m Model) renderScrollIndicator(totalRules int, scrollOffset int) string {
+ current := scrollOffset + 1
+ end := min(scrollOffset+MaxVisibleRules, totalRules)
+ return m.styles.Label.Foreground(lipgloss.Color("241")).Render(
+ fmt.Sprintf(" Showing %d-%d of %d", current, end, totalRules))
+}
+
func truncate(s string, maxLen int) string {
s = strings.TrimSuffix(s, " (v6)")
if len(s) > maxLen {
@@ -172,15 +192,15 @@ func (m Model) DeleteConfirmView() string {
lines = append(lines, "")
lines = append(lines, m.styles.Error.Render(" Are you sure you want to delete this rule?"))
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 %s", m.styles.Label.Render("Action:"), ui.GetPolicyStyle(m.styles, r.Action).Render(r.Action), m.styles.Value.Render(r.ToPort)))
- lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("From:"), m.styles.Value.Render(r.FromSource)))
- lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("To:"), m.styles.Value.Render(r.ToDest)))
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Width(10).Render("Rule :"), m.styles.Value.Render(fmt.Sprintf("%d", r.Num))))
+ lines = append(lines, fmt.Sprintf(" %s %s %s", m.styles.Label.Width(10).Render("Action :"), ui.GetPolicyStyle(m.styles, r.Action).Render(r.Action), m.styles.Value.Render(r.ToPort)))
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Width(10).Render("From :"), m.styles.Value.Render(r.FromSource)))
+ lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Width(10).Render("To :"), m.styles.Value.Render(r.ToDest)))
lines = append(lines, "")
text := m.styles.Label.
PaddingTop(1).
UnsetWidth().
- Render("[y] Yes, delete [n/Esc] Cancel")
+ Render("[Enter] Confirm [Esc] Cancel")
centered := lipgloss.Place(
50,
internal/ui/titled_box.go
@@ -6,7 +6,7 @@ import (
"github.com/charmbracelet/lipgloss"
)
-func TitledBox(title, content string, styles Styles, width int, activeSession bool) string {
+func TitledBox(title, content string, styles Styles, width int, activeSession bool, height ...int) string {
style := styles.SectionBorder
if activeSession {
@@ -26,6 +26,9 @@ func TitledBox(title, content string, styles Styles, width int, activeSession bo
if boxWidth > 0 {
style = style.Width(boxWidth)
}
+ if len(height) > 0 {
+ style = style.Height(height[0])
+ }
box := style.Render(content)