1package rules
2
3import (
4 "fmt"
5 "regexp"
6 "strings"
7 "github.com/The-Robin-Hood/ufWall/internal/ufw"
8 "github.com/The-Robin-Hood/ufWall/internal/ui"
9
10 "github.com/charmbracelet/lipgloss"
11)
12
13type RulesData struct {
14 IPv4 []ufw.Rule
15 IPv6 []ufw.Rule
16}
17
18func (m Model) View(data RulesData) string {
19 sectionActiveNoMenu := m.menu == nil && m.active && !m.showDetails && !m.showDeleteConfirm
20
21 ipTable := m.renderTable(data.IPv4, data.IPv6, sectionActiveNoMenu)
22
23 content := lipgloss.JoinVertical(
24 lipgloss.Left,
25 ipTable,
26 )
27
28 return ui.TitledBox("Active Rules", content, m.styles, -1, m.active, 13)
29}
30
31func (m Model) renderTable(ipV4Rules []ufw.Rule, ipV6Rules []ufw.Rule, isActive bool) string {
32 var rows []string
33
34 titleStyle := m.styles.Label
35 if isActive {
36 titleStyle = titleStyle.Bold(true).Foreground(lipgloss.Color("12"))
37 }
38
39 activeTitle := "IPv4 Rules"
40 inActiveTitle := "IPv6 Rules"
41
42 rules := ipV4Rules
43 scrollOffset := m.ipv4ScrollOffset
44 cursorLine := m.ipv4CursorLine
45
46 maxIPv4 := len(fmt.Sprintf("%s (%d)", "IPv4 Rules", 9999))
47 maxIPv6 := len(fmt.Sprintf("%s (%d)", "IPv6 Rules", 9999))
48
49 padTitle := func(title string, count int, width int) string {
50 return fmt.Sprintf("%-*s", width, fmt.Sprintf("%s (%d)", title, count))
51 }
52
53 tabTitle := titleStyle.Render(padTitle(activeTitle, len(ipV4Rules), maxIPv4)) + "| " +
54 m.styles.Label.Foreground(lipgloss.Color("241")).Render(padTitle(inActiveTitle, len(ipV6Rules), maxIPv6))
55
56 if m.activeTable == IPv6Table {
57 scrollOffset = m.ipv6ScrollOffset
58 cursorLine = m.ipv6CursorLine
59 rules = ipV6Rules
60 tabTitle = m.styles.Label.Foreground(lipgloss.Color("241")).Render(padTitle(activeTitle, len(ipV4Rules), maxIPv4)) + "| " +
61 titleStyle.Render(padTitle(inActiveTitle, len(ipV6Rules), maxIPv6))
62 }
63
64 if len(rules) > MaxVisibleRules {
65 scrollInfo := m.renderScrollIndicator(len(rules), scrollOffset)
66 lineWidth := lipgloss.Width(tabTitle)
67 scrollInfoRight := lipgloss.PlaceHorizontal(lineWidth, lipgloss.Right, scrollInfo)
68 tabTitle = lipgloss.JoinHorizontal(lipgloss.Left, tabTitle, scrollInfoRight)
69 }
70
71 rows = append(rows, tabTitle)
72 rows = append(rows, "")
73
74 if len(rules) == 0 {
75 emptyMsg := m.styles.Label.Foreground(lipgloss.Color("241")).Render(" (no rules)")
76 rows = append(rows, emptyMsg)
77 return strings.Join(rows, "\n")
78 }
79
80 header := fmt.Sprintf(
81 "%-3s │ %-6s │ %-5s │ %-16s │ %-5s │ %-16s │ %-5s",
82 "#", "Action", "Proto", "Source", "sPort", "Destination", "dPort")
83
84 headerContent := m.styles.Label.UnsetWidth().Render(header)
85 line := strings.Repeat("─", lipgloss.Width(headerContent))
86 rows = append(rows, " "+headerContent, " "+m.styles.Label.UnsetWidth().Render(line))
87
88 startIdx := scrollOffset
89 endIdx := min(scrollOffset+MaxVisibleRules, len(rules))
90
91 for i := startIdx; i < endIdx; i++ {
92 r := rules[i]
93 action := fmt.Sprintf("%-6s", r.Action)
94 row := fmt.Sprintf(
95 "%-3d │ %6s │ %-5s │ %-16s │ %-5s │ %-16s │ %-5s",
96 r.Num,
97 action,
98 r.ToProtocol,
99 truncate(r.FromSource, 16),
100 truncate(r.FromPort, 5),
101 truncate(r.ToDest, 16),
102 truncate(r.ToPort, 5),
103 )
104 rows = append(rows, ui.InsertCursorRulesSection(row, cursorLine == i && isActive, m.styles, r.Action))
105 }
106 return strings.Join(rows, "\n")
107}
108
109func (m Model) renderScrollIndicator(totalRules int, scrollOffset int) string {
110 current := scrollOffset + 1
111 end := min(scrollOffset+MaxVisibleRules, totalRules)
112 return m.styles.Label.Foreground(lipgloss.Color("241")).Render(
113 fmt.Sprintf(" Showing %d-%d of %d", current, end, totalRules))
114}
115
116func truncate(s string, maxLen int) string {
117 s = strings.TrimSuffix(s, " (v6)")
118 if len(s) > maxLen {
119 return s[:maxLen-1] + "…"
120 }
121 return s
122}
123
124func (m Model) DetailView() string {
125 if m.detailRule == nil {
126 return ""
127 }
128
129 r := m.detailRule
130
131 var lines []string
132 lines = append(lines, "")
133 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Rule #:"), m.styles.Value.Render(fmt.Sprintf("%d", r.Num))))
134 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Action:"), ui.GetPolicyStyle(m.styles, r.Action).Render(r.Action)))
135 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Protocol:"), m.styles.Value.Render(r.ToProtocol)))
136
137 ipVersion := "IPv4"
138 if r.IPv6 {
139 ipVersion = "IPv6"
140 }
141 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("IP Version:"), m.styles.Value.Render(ipVersion)))
142
143 lines = append(lines, "")
144 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Source:"), m.styles.Value.Render(r.FromSource)))
145 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Source Port:"), m.styles.Value.Render(r.FromPort)))
146 lines = append(lines, "")
147 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Destination:"), m.styles.Value.Render(r.ToDest)))
148 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Dest Port:"), m.styles.Value.Render(r.ToPort)))
149 lines = append(lines, "")
150
151 if r.Comment != "" {
152 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Comment:"), m.styles.Value.Render(r.Comment)))
153 lines = append(lines, "")
154 }
155
156 re := regexp.MustCompile(`\s+`)
157 rawText := re.ReplaceAllString(r.Raw, " ")
158 lines = append(lines, fmt.Sprintf(" %s", m.styles.Label.Render("Raw:")))
159 lines = append(lines, fmt.Sprintf(" %s", m.styles.Value.Render(rawText)))
160 lines = append(lines, "")
161 text := m.styles.Label.
162 PaddingTop(1).
163 UnsetWidth().
164 Render("[Press any key to close]")
165
166 centered := lipgloss.Place(
167 50,
168 1,
169 lipgloss.Center,
170 lipgloss.Center,
171 text,
172 )
173
174 lines = append(lines, centered)
175
176 lines = append(lines, "")
177
178 content := strings.Join(lines, "\n")
179
180 title := fmt.Sprintf("Rule #%d Details", r.Num)
181 return ui.TitledBox(title, content, m.styles, -1, true)
182}
183
184func (m Model) DeleteConfirmView() string {
185 if m.deleteRule == nil {
186 return ""
187 }
188
189 r := m.deleteRule
190
191 var lines []string
192 lines = append(lines, "")
193 lines = append(lines, m.styles.Error.Render(" Are you sure you want to delete this rule?"))
194 lines = append(lines, "")
195 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Width(10).Render("Rule :"), m.styles.Value.Render(fmt.Sprintf("%d", r.Num))))
196 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)))
197 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Width(10).Render("From :"), m.styles.Value.Render(r.FromSource)))
198 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Width(10).Render("To :"), m.styles.Value.Render(r.ToDest)))
199 lines = append(lines, "")
200 text := m.styles.Label.
201 PaddingTop(1).
202 UnsetWidth().
203 Render("[Enter] Confirm [Esc] Cancel")
204
205 centered := lipgloss.Place(
206 50,
207 1,
208 lipgloss.Center,
209 lipgloss.Center,
210 text,
211 )
212
213 lines = append(lines, centered)
214 lines = append(lines, "")
215
216 content := strings.Join(lines, "\n")
217
218 return ui.TitledBox("Confirm Delete", content, m.styles, -1, true)
219}
220
221func (m Model) AddWizardView() string {
222 w := m.addWizard
223 if w == nil {
224 return ""
225 }
226
227 var lines []string
228 lines = append(lines, "")
229
230 if w.Params.Action != "" {
231 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Action:"), ui.GetPolicyStyle(m.styles, w.Params.Action).Render(w.Params.Action)))
232 }
233 if w.Step > StepDirection && w.Params.Direction != "" {
234 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Direction:"), m.styles.Value.Render(w.Params.Direction)))
235 } else if w.Step > StepDirection {
236 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Direction:"), m.styles.Value.Render("both")))
237 }
238 if w.Step > StepProtocol {
239 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Protocol:"), m.styles.Value.Render(w.Params.Protocol)))
240 }
241 if w.Step > StepPort {
242 port := w.Params.Port
243 if port == "" {
244 port = "any"
245 }
246 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Port:"), m.styles.Value.Render(port)))
247 }
248 if w.Step > StepSource {
249 src := w.Params.FromAddr
250 if src == "" {
251 src = "any"
252 }
253 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Source:"), m.styles.Value.Render(src)))
254 }
255 if w.Step > StepDestination {
256 dst := w.Params.ToAddr
257 if dst == "" {
258 dst = "any"
259 }
260 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Destination:"), m.styles.Value.Render(dst)))
261 }
262 if w.Step > StepInterface {
263 iface := w.Params.Interface
264 if iface == "" {
265 iface = "all"
266 }
267 lines = append(lines, fmt.Sprintf(" %s %s", m.styles.Label.Render("Interface:"), m.styles.Value.Render(iface)))
268 }
269
270 if len(lines) > 1 {
271 lines = append(lines, "")
272 }
273
274 stepTitle := m.getWizardStepTitle(w.Step)
275 lines = append(lines, m.styles.Title.Render(fmt.Sprintf(" %s", stepTitle)))
276 lines = append(lines, "")
277
278 if w.Error != "" {
279 lines = append(lines, m.styles.Error.Render(fmt.Sprintf(" Error: %s", w.Error)))
280 lines = append(lines, "")
281 }
282
283 if w.InputMode {
284 inputBox := fmt.Sprintf(" > %s_", w.Input)
285 lines = append(lines, m.styles.Value.Render(inputBox))
286 lines = append(lines, "")
287 lines = append(lines, m.styles.Label.Foreground(lipgloss.Color("241")).Render(" [Enter] Confirm [Esc] Cancel"))
288 } else {
289 for i, opt := range w.Options {
290 prefix := " "
291 style := m.styles.Label
292 if i == w.Cursor {
293 prefix = "> "
294 style = m.styles.Value.Bold(true)
295 }
296 lines = append(lines, style.Render(fmt.Sprintf(" %s%s", prefix, opt)))
297 }
298 lines = append(lines, "")
299
300 hints := "[Enter] Select [Esc] Cancel"
301 if w.Step > StepAction {
302 hints = "[b] Back [Esc] Cancel"
303 }
304 if w.Step == StepPort || w.Step == StepSource || w.Step == StepDestination {
305 hints = "[c] Custom [b] Back [Esc] Cancel"
306 }
307 lines = append(lines, m.styles.Label.UnsetWidth().Foreground(lipgloss.Color("241")).Render(hints))
308 }
309
310 lines = append(lines, "")
311
312 // Show preview command at confirm step
313 // if w.Step == StepConfirm {
314 // args := ufw.BuildAddRuleCommand(w.Params)
315 // cmdPreview := "sudo ufw " + strings.Join(args, " ")
316 // lines = append(lines, m.styles.Label.Render(" Command:"))
317 // lines = append(lines, m.styles.Value.Render(fmt.Sprintf(" %s", cmdPreview)))
318 // lines = append(lines, "")
319 // }
320
321 content := strings.Join(lines, "\n")
322 return ui.TitledBox("Add New Rule", content, m.styles, 40, true)
323}
324
325func (m Model) getWizardStepTitle(step WizardStep) string {
326 switch step {
327 case StepAction:
328 return "Select Action"
329 case StepDirection:
330 return "Select Direction"
331 case StepProtocol:
332 return "Select Protocol"
333 case StepPort:
334 return "Select Port"
335 case StepSource:
336 return "Select Source Address"
337 case StepDestination:
338 return "Select Destination Address"
339 case StepInterface:
340 return "Select Interface"
341 case StepConfirm:
342 return "Confirm Rule"
343 default:
344 return "Add Rule"
345 }
346}