Skip to content

Commit 8e48c26

Browse files
committed
Starting implementation of a "responsive" TUI
1 parent 06a0940 commit 8e48c26

File tree

5 files changed

+143
-80
lines changed

5 files changed

+143
-80
lines changed

tui/models/bisturi_model.go

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type errMsg error
2929
type packetMsg sockets.NetworkPacket
3030

3131
type bisturiModel struct {
32+
terminalHeight int
33+
terminalWidth int
3234
step step
3335
spinner spinner.Model
3436
startMenu startMenuModel
@@ -43,20 +45,23 @@ type bisturiModel struct {
4345
err error
4446
}
4547

46-
func NewBisturiModel() *bisturiModel {
47-
s := spinner.New(spinner.WithSpinner(spinner.Meter))
48-
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99"))
49-
48+
func newRowsInput(terminalWidth int) textinput.Model {
5049
ti := textinput.New()
5150
ti.Placeholder = "Enter the max number of rows to display"
5251
ti.Focus()
5352
ti.CharLimit = 4
54-
ti.Width = 50
53+
ti.Width = terminalWidth / 2
54+
55+
return ti
56+
}
57+
58+
func NewBisturiModel() *bisturiModel {
59+
s := spinner.New(spinner.WithSpinner(spinner.Meter))
60+
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99"))
5561

5662
return &bisturiModel{
57-
step: retrieveIfaces,
58-
spinner: s,
59-
rowsInput: ti,
63+
step: retrieveIfaces,
64+
spinner: s,
6065
}
6166
}
6267

@@ -93,7 +98,7 @@ func (m bisturiModel) View() string {
9398

9499
switch m.step {
95100
case retrieveIfaces:
96-
sb.WriteString(fmt.Sprintf("\n\nWelcome!\n\n %s Retrieving network interfaces...\n\n", m.spinner.View()))
101+
sb.WriteString(fmt.Sprintf("\n\nWelcome!\n\nRetrieving network interfaces \n\n%s", m.spinner.View()))
97102
case selectIface, selectProtocol:
98103
sb.WriteString(m.startMenu.View())
99104
case selectRows:
@@ -113,8 +118,14 @@ func (m *bisturiModel) updateLoading(msg tea.Msg) (tea.Model, tea.Cmd) {
113118
m.err = msg
114119
return m, tea.Quit
115120

121+
case tea.WindowSizeMsg:
122+
m.terminalHeight = msg.Height
123+
m.terminalWidth = msg.Width
124+
125+
return m, nil
126+
116127
case networkInterfacesMsg:
117-
m.startMenu = newStartMenuModel(msg)
128+
m.startMenu = newStartMenuModel(msg, m.terminalHeight, m.terminalWidth)
118129
m.step = selectIface
119130

120131
return m, nil
@@ -136,6 +147,17 @@ func (m *bisturiModel) updateStartMenuSelection(msg tea.Msg) (tea.Model, tea.Cmd
136147
m.startMenu, cmd = m.startMenu.Update(msg)
137148

138149
switch msg := msg.(type) {
150+
case tea.WindowSizeMsg:
151+
m.terminalHeight = msg.Height
152+
m.terminalWidth = msg.Height
153+
if m.step == selectIface {
154+
m.startMenu.ifaceList.resize(m.terminalHeight, m.terminalWidth)
155+
} else if m.step == selectProtocol {
156+
m.startMenu.protoList.resize(m.terminalHeight, m.terminalWidth)
157+
}
158+
159+
return m, nil
160+
139161
case selectedIfaceItemMsg:
140162
iface, err := net.InterfaceByName(msg.name)
141163
if err != nil {
@@ -165,6 +187,7 @@ func (m *bisturiModel) updateStartMenuSelection(msg tea.Msg) (tea.Model, tea.Cmd
165187
m.rawSocket = rs
166188
m.step = selectRows
167189

190+
m.rowsInput = newRowsInput(m.terminalWidth)
168191
return m, nil
169192
}
170193
return m, cmd
@@ -175,6 +198,12 @@ func (m *bisturiModel) updateRowsInput(msg tea.Msg) (tea.Model, tea.Cmd) {
175198
m.rowsInput, cmd = m.rowsInput.Update(msg)
176199

177200
switch msg := msg.(type) {
201+
case tea.WindowSizeMsg:
202+
m.terminalHeight = msg.Height
203+
m.terminalWidth = msg.Width
204+
205+
return m, nil
206+
178207
case tea.KeyMsg:
179208
switch msg.String() {
180209
case "q", "esc", "ctrl+c":
@@ -183,7 +212,7 @@ func (m *bisturiModel) updateRowsInput(msg tea.Msg) (tea.Model, tea.Cmd) {
183212
case "enter":
184213
maxRows, err := strconv.Atoi(m.rowsInput.Value())
185214
if err == nil && maxRows > 0 {
186-
m.packetsTable = newPacketsTable(maxRows)
215+
m.packetsTable = newPacketsTable(maxRows, m.terminalWidth)
187216

188217
m.packetsChan = make(chan sockets.NetworkPacket)
189218
m.errChan = make(chan error)
@@ -206,7 +235,14 @@ func (m *bisturiModel) updateReceivingPacket(msg tea.Msg) (tea.Model, tea.Cmd) {
206235

207236
m.packetsTable, cmd = m.packetsTable.Update(msg)
208237

209-
switch msg.(type) {
238+
switch msg := msg.(type) {
239+
case tea.WindowSizeMsg:
240+
m.terminalHeight = msg.Height
241+
m.terminalWidth = msg.Width
242+
m.packetsTable.resizeTable(m.terminalWidth)
243+
244+
return m, nil
245+
210246
case sockets.NetworkPacket:
211247
return m, m.waitForPacket
212248
}

tui/models/interfaces_list.go

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,40 @@ type interfacesListModel struct {
2929
l list.Model
3030
}
3131

32+
func newInterfacesListModel(interfaces []net.Interface, terminalHeight, terminalWidth int) interfacesListModel {
33+
listHeight := (75 * terminalHeight) / 100
34+
listWidth := (75 * terminalWidth) / 100
35+
36+
items := make([]list.Item, len(interfaces))
37+
for i, iface := range interfaces {
38+
items[i] = ifaceItem{
39+
name: iface.Name,
40+
flags: iface.Flags.String(),
41+
}
42+
}
43+
44+
titlesStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99")).Blink(true).Bold(true)
45+
ifaceDelegate := list.NewDefaultDelegate()
46+
ifaceDelegate.Styles.SelectedTitle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99"))
47+
48+
ifaceList := list.New(items, ifaceDelegate, listWidth, listHeight)
49+
ifaceList.Title = "Select a Network Interface"
50+
ifaceList.Styles.Title = titlesStyle
51+
ifaceList.SetShowStatusBar(true)
52+
ifaceList.SetFilteringEnabled(false)
53+
ifaceList.SetShowHelp(true)
54+
55+
return interfacesListModel{l: ifaceList}
56+
}
57+
58+
func (m *interfacesListModel) resize(terminalHeight, terminalWidth int) {
59+
listHeight := (75 * terminalHeight) / 100
60+
listWidth := (75 * terminalWidth) / 100
61+
62+
m.l.SetHeight(listHeight)
63+
m.l.SetWidth(listWidth)
64+
}
65+
3266
func (m interfacesListModel) Init() tea.Cmd {
3367
return nil
3468
}
@@ -57,29 +91,6 @@ func (m interfacesListModel) View() string {
5791
return m.l.View()
5892
}
5993

60-
func newInterfacesListModel(width, height int, interfaces []net.Interface) interfacesListModel {
61-
items := make([]list.Item, len(interfaces))
62-
for i, iface := range interfaces {
63-
items[i] = ifaceItem{
64-
name: iface.Name,
65-
flags: iface.Flags.String(),
66-
}
67-
}
68-
69-
titlesStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99")).Blink(true).Bold(true)
70-
ifaceDelegate := list.NewDefaultDelegate()
71-
ifaceDelegate.Styles.SelectedTitle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99"))
72-
73-
ifaceList := list.New(items, ifaceDelegate, width, height)
74-
ifaceList.Title = "Select a Network Interface"
75-
ifaceList.Styles.Title = titlesStyle
76-
ifaceList.SetShowStatusBar(true)
77-
ifaceList.SetFilteringEnabled(false)
78-
ifaceList.SetShowHelp(true)
79-
80-
return interfacesListModel{l: ifaceList}
81-
}
82-
8394
func fetchInterfaces() tea.Cmd {
8495
return func() tea.Msg {
8596
ifaces, err := net.Interfaces()

tui/models/packets_table.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,36 @@ type packetsTablemodel struct {
2525
counter uint64
2626
}
2727

28-
func newPacketsTable(max int) packetsTablemodel {
28+
func buildTable(rows []table.Row, terminalWidth int) table.Model {
29+
return table.New([]table.Column{
30+
table.NewColumn(columnKeyID, "#", (2*terminalWidth)/100),
31+
table.NewColumn(columnKeyDate, "Date", (8*terminalWidth)/100),
32+
table.NewColumn(columnKeySource, "Source", (20*terminalWidth)/100),
33+
table.NewColumn(columnKeyDestination, "Destination", (20*terminalWidth)/100),
34+
table.NewColumn(columnKeyInfo, "Info", (46*terminalWidth)/100),
35+
}).
36+
WithRows(rows).
37+
WithBaseStyle(lipgloss.NewStyle().
38+
BorderForeground(lipgloss.Color("#00cc99")).
39+
Foreground(lipgloss.Color("#00cc99")).
40+
Align(lipgloss.Center),
41+
)
42+
}
43+
44+
func newPacketsTable(max int, terminalWidth int) packetsTablemodel {
2945
rows := make([]table.Row, 0, max)
3046

3147
return packetsTablemodel{
3248
maxRows: max,
3349
cachedRows: rows,
34-
table: table.New([]table.Column{
35-
table.NewColumn(columnKeyID, "#", 5),
36-
table.NewColumn(columnKeyDate, "Date", 18),
37-
table.NewColumn(columnKeySource, "Source", 50),
38-
table.NewColumn(columnKeyDestination, "Destination", 50),
39-
table.NewColumn(columnKeyInfo, "Info", 100),
40-
}).
41-
WithRows(rows).
42-
WithBaseStyle(lipgloss.NewStyle().
43-
BorderForeground(lipgloss.Color("#00cc99")).
44-
Foreground(lipgloss.Color("#00cc99")).
45-
Align(lipgloss.Center),
46-
),
50+
table: buildTable(rows, terminalWidth),
4751
}
4852
}
4953

54+
func (m *packetsTablemodel) resizeTable(terminalWidth int) {
55+
m.table = buildTable(m.cachedRows, terminalWidth)
56+
}
57+
5058
func (m packetsTablemodel) Init() tea.Cmd {
5159
return nil
5260
}

tui/models/protocols_list.go

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,42 @@ type protocolsListModel struct {
2727
l list.Model
2828
}
2929

30+
func newProtocolsListModel(terminalHeight, terminalWidth int) protocolsListModel {
31+
listHeight := (75 * terminalHeight) / 100
32+
listWidth := (75 * terminalWidth) / 100
33+
34+
protoDelegate := list.NewDefaultDelegate()
35+
protoDelegate.Styles.SelectedTitle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99"))
36+
37+
items := []list.Item{
38+
protoItem{name: "all", ethType: syscall.ETH_P_ALL},
39+
protoItem{name: "arp", ethType: syscall.ETH_P_ARP},
40+
protoItem{name: "ip", ethType: syscall.ETH_P_IP},
41+
protoItem{name: "ipv6", ethType: syscall.ETH_P_IPV6},
42+
// UDP and TCP are part of IP, need special handling if filtered specifically
43+
protoItem{name: "udp", ethType: syscall.ETH_P_IP},
44+
protoItem{name: "udp6", ethType: syscall.ETH_P_IPV6},
45+
protoItem{name: "tcp", ethType: syscall.ETH_P_IP},
46+
protoItem{name: "tcp6", ethType: syscall.ETH_P_IPV6},
47+
}
48+
protoList := list.New(items, protoDelegate, listWidth, listHeight)
49+
protoList.Title = "Select a Network Protocol"
50+
protoList.Styles.Title = lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99")).Blink(true).Bold(true)
51+
protoList.SetShowStatusBar(false)
52+
protoList.SetFilteringEnabled(false)
53+
protoList.SetShowHelp(true)
54+
55+
return protocolsListModel{l: protoList}
56+
}
57+
58+
func (m *protocolsListModel) resize(terminalHeight, terminalWidth int) {
59+
listHeight := (75 * terminalHeight) / 100
60+
listWidth := (75 * terminalWidth) / 100
61+
62+
m.l.SetHeight(listHeight)
63+
m.l.SetWidth(listWidth)
64+
}
65+
3066
func (m protocolsListModel) Init() tea.Cmd {
3167
return nil
3268
}
@@ -54,28 +90,3 @@ func (m protocolsListModel) Update(msg tea.Msg) (protocolsListModel, tea.Cmd) {
5490
func (m protocolsListModel) View() string {
5591
return m.l.View()
5692
}
57-
58-
func newProtocolsListModel(width, height int) protocolsListModel {
59-
protoDelegate := list.NewDefaultDelegate()
60-
protoDelegate.Styles.SelectedTitle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99"))
61-
62-
items := []list.Item{
63-
protoItem{name: "all", ethType: syscall.ETH_P_ALL},
64-
protoItem{name: "arp", ethType: syscall.ETH_P_ARP},
65-
protoItem{name: "ip", ethType: syscall.ETH_P_IP},
66-
protoItem{name: "ipv6", ethType: syscall.ETH_P_IPV6},
67-
// UDP and TCP are part of IP, need special handling if filtered specifically
68-
protoItem{name: "udp", ethType: syscall.ETH_P_IP},
69-
protoItem{name: "udp6", ethType: syscall.ETH_P_IPV6},
70-
protoItem{name: "tcp", ethType: syscall.ETH_P_IP},
71-
protoItem{name: "tcp6", ethType: syscall.ETH_P_IPV6},
72-
}
73-
protoList := list.New(items, protoDelegate, width, height)
74-
protoList.Title = "Select a Network Protocol"
75-
protoList.Styles.Title = lipgloss.NewStyle().Foreground(lipgloss.Color("#00cc99")).Blink(true).Bold(true)
76-
protoList.SetShowStatusBar(false)
77-
protoList.SetFilteringEnabled(false)
78-
protoList.SetShowHelp(true)
79-
80-
return protocolsListModel{l: protoList}
81-
}

tui/models/start_menu.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,9 @@ type selectedProtocolMsg struct {
2121
ethTytpe uint16
2222
}
2323

24-
func newStartMenuModel(interfaces []net.Interface) startMenuModel {
25-
const listHeight = 50
26-
const listWidth = 50
27-
28-
il := newInterfacesListModel(listWidth, listHeight, interfaces)
29-
plm := newProtocolsListModel(listWidth, listHeight)
24+
func newStartMenuModel(interfaces []net.Interface, terminalHeight, terminalWidth int) startMenuModel {
25+
il := newInterfacesListModel(interfaces, terminalHeight, terminalWidth)
26+
plm := newProtocolsListModel(terminalHeight, terminalWidth)
3027

3128
return startMenuModel{
3229
step: selectIface,

0 commit comments

Comments
 (0)