main
6d86999 · 6 months ago 26 commits
  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}