Skip to content

Commit daaddcd

Browse files
committed
Fix kill shortcut (x/K), dynamic pagination, and improve UI
1 parent 4fc8c1f commit daaddcd

3 files changed

Lines changed: 65 additions & 41 deletions

File tree

cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ Keyboard Shortcuts (TUI mode):
219219
↑/↓ or j/k Navigate list
220220
PgUp/PgDn Change page
221221
Enter View process details
222-
k Kill selected process
222+
x / Shift+K Kill selected process
223223
r Refresh port list
224224
/ Search/filter mode
225225
h Show help

internal/ports/scanner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ func containsState(states []string, state string) bool {
165165

166166
// Refresh reloads the connection data
167167
func (s *Scanner) Refresh() ([]models.PortEntry, error) {
168-
return s.GetConnections([]string{"listen", "established"})
168+
return s.GetConnections([]string{"LISTENING", "ESTABLISHED"})
169169
}
170170

171171
// GetPortEntry finds a specific port entry by PID and port

internal/tui/model.go

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ type Model struct {
3636
page int
3737
pageSize int
3838

39+
// Terminal dimensions
40+
width int
41+
height int
42+
3943
// Admin status
4044
isAdmin bool
4145

@@ -51,6 +55,9 @@ type Model struct {
5155
selectedEntry models.PortEntry
5256
}
5357

58+
// clearMsgTick is sent after a delay to clear status messages
59+
type clearMsgTick struct{}
60+
5461
// InitialModel creates the initial TUI model
5562
func InitialModel() *Model {
5663
scanner := ports.NewScanner()
@@ -74,6 +81,8 @@ func InitialModel() *Model {
7481
successMsg: "",
7582
page: 0,
7683
pageSize: 20,
84+
width: 80,
85+
height: 24,
7786
isAdmin: isAdmin,
7887
scanner: scanner,
7988
kill: kill,
@@ -86,6 +95,28 @@ func (m *Model) Init() tea.Cmd {
8695
return m.refresh()
8796
}
8897

98+
// scheduleClearMsg returns a command that clears messages after a delay
99+
func scheduleClearMsg() tea.Cmd {
100+
return tea.Tick(3*time.Second, func(_ time.Time) tea.Msg {
101+
return clearMsgTick{}
102+
})
103+
}
104+
105+
// calcPageSize computes how many rows fit based on terminal height
106+
func (m *Model) calcPageSize() {
107+
// Reserve lines: header(2) + admin warning(2) + pagination(1) + spacing(1) +
108+
// table header(1) + footer(2) + some buffer(2) = ~11 overhead lines
109+
overhead := 11
110+
if !m.isAdmin {
111+
overhead += 2 // admin warning takes extra lines
112+
}
113+
ps := m.height - overhead
114+
if ps < 5 {
115+
ps = 5
116+
}
117+
m.pageSize = ps
118+
}
119+
89120
// Update handles messages
90121
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
91122
switch msg := msg.(type) {
@@ -107,19 +138,18 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107138
m.isLoading = false
108139
m.refreshing = false
109140
m.mu.Unlock()
141+
return m, scheduleClearMsg()
142+
143+
case clearMsgTick:
144+
m.errMsg = ""
145+
m.successMsg = ""
110146
return m, nil
111147

112148
case tea.WindowSizeMsg:
113-
// Handle window resize if needed
114-
}
115-
116-
// Clear messages after delay
117-
if m.errMsg != "" || m.successMsg != "" {
118-
go func() {
119-
time.Sleep(3 * time.Second)
120-
// Note: We can't directly modify model from goroutine
121-
// This would need to be handled differently in production
122-
}()
149+
m.width = msg.Width
150+
m.height = msg.Height
151+
m.calcPageSize()
152+
return m, nil
123153
}
124154

125155
return m, nil
@@ -156,7 +186,7 @@ func (m *Model) handleKeyPress(msg tea.KeyMsg) (*Model, tea.Cmd) {
156186
case "ctrl+c", "q":
157187
return m, tea.Quit
158188

159-
case "k":
189+
case "x", "K": // Kill with x or Shift+K (not k, which is vim-up)
160190
m2, cmd := m.handleKill()
161191
return m2, cmd
162192

@@ -172,13 +202,13 @@ func (m *Model) handleKeyPress(msg tea.KeyMsg) (*Model, tea.Cmd) {
172202
m.showHelp = true
173203
return m, nil
174204

175-
case "up", "w": // vi style - up arrow or w
205+
case "up", "k": // vim style: k = up
176206
if m.selected > 0 {
177207
m.selected--
178208
}
179209
return m, nil
180210

181-
case "down", "j": // vi style
211+
case "down", "j": // vim style: j = down
182212
if m.selected < len(m.getCurrentPageEntries())-1 {
183213
m.selected++
184214
}
@@ -283,14 +313,7 @@ func (m *Model) handleKill() (*Model, tea.Cmd) {
283313
pageEntries := m.getCurrentPageEntries()
284314
if len(pageEntries) == 0 || m.selected >= len(pageEntries) {
285315
m.errMsg = "No process selected"
286-
return m, nil
287-
}
288-
289-
entry := pageEntries[m.selected]
290-
291-
if entry.IsSystem {
292-
m.showKillConfirm = true
293-
return m, nil
316+
return m, scheduleClearMsg()
294317
}
295318

296319
m.showKillConfirm = true
@@ -309,10 +332,10 @@ func (m *Model) handleKillConfirm(msg tea.KeyMsg) (*Model, tea.Cmd) {
309332
result := m.kill.Kill(entry.PID)
310333
if result.Success {
311334
m.successMsg = result.Message
312-
return m, m.refresh()
313-
} else {
314-
m.errMsg = result.Message
335+
return m, tea.Batch(m.refresh(), scheduleClearMsg())
315336
}
337+
m.errMsg = result.Message
338+
return m, scheduleClearMsg()
316339
}
317340

318341
case "n", "N", "esc":
@@ -401,12 +424,12 @@ func (m *Model) View() string {
401424
// Pagination info
402425
if len(m.filtered) > 0 {
403426
sb.WriteString(paginationStyle.Render(fmt.Sprintf("Page %d/%d • %d entries", m.page+1, m.totalPages(), len(m.filtered))))
404-
} else {
405-
sb.WriteString("\n")
406427
}
428+
sb.WriteString("\n")
407429

408430
// Loading state
409431
if m.isLoading {
432+
sb.WriteString("\n")
410433
sb.WriteString(loadingStyle.Render("Loading..."))
411434
return sb.String()
412435
}
@@ -429,29 +452,29 @@ func (m *Model) View() string {
429452
// Search mode
430453
if m.searchMode {
431454
sb.WriteString(searchStyle.Render(fmt.Sprintf("Search: %s", m.searchQuery)))
432-
sb.WriteString(" (Esc to cancel)\n\n")
455+
sb.WriteString(" (Esc to cancel)\n")
433456
}
434457

435458
// Error message
436459
if m.errMsg != "" {
437460
sb.WriteString(errorStyle.Render(m.errMsg))
438-
sb.WriteString("\n\n")
439-
m.errMsg = "" // Clear after displaying
461+
sb.WriteString("\n")
440462
}
441463

442464
// Success message
443465
if m.successMsg != "" {
444466
sb.WriteString(successStyle.Render(m.successMsg))
445-
sb.WriteString("\n\n")
446-
m.successMsg = "" // Clear after displaying
467+
sb.WriteString("\n")
447468
}
448469

470+
sb.WriteString("\n")
471+
449472
// Table header
450473
sb.WriteString(tableHeaderStyle.Render(
451474
fmt.Sprintf("%s %s %s %s %s",
452475
padRight("PROTO", 5),
453476
padRight("PORT", 6),
454-
padRight("PID", 6),
477+
padRight("PID", 8),
455478
padRight("PROCESS", 20),
456479
"STATE"),
457480
))
@@ -477,7 +500,7 @@ func (m *Model) View() string {
477500
// Footer
478501
sb.WriteString("\n")
479502
sb.WriteString(footerStyle.Render(
480-
"[↑/↓] Navigate [PgUp/PgDn] Page [k] Kill [r] Refresh [/] Filter [h] Help [q] Quit",
503+
"[↑/↓/j/k] Navigate [PgUp/PgDn] Page [x] Kill [r] Refresh [/] Filter [h] Help [q] Quit",
481504
))
482505

483506
return sb.String()
@@ -497,7 +520,7 @@ func (m *Model) renderRow(entry models.PortEntry) string {
497520
return fmt.Sprintf("%s %s %s %s %s",
498521
protocolStyle.Render(padRight(entry.Protocol, 5)),
499522
padRight(fmt.Sprintf("%d", entry.Port), 6),
500-
padRight(fmt.Sprintf("%d", entry.PID), 6),
523+
padRight(fmt.Sprintf("%d", entry.PID), 8),
501524
padRight(truncate(entry.ProcessName, 18), 20),
502525
stateStyle.Render(entry.State),
503526
)
@@ -524,11 +547,11 @@ func (m *Model) renderHelp() string {
524547
{"↑/↓ or j/k", "Navigate list"},
525548
{"PgUp/PgDn", "Change page"},
526549
{"Enter", "View process details"},
527-
{"k", "Kill selected process"},
550+
{"x / Shift+K", "Kill selected process"},
528551
{"r", "Refresh port list"},
529552
{"/", "Search/filter mode"},
530553
{"h", "Show this help"},
531-
{"q", "Quit"},
554+
{"q / Ctrl+C", "Quit"},
532555
{"Esc", "Clear filter / Close dialog"},
533556
}
534557

@@ -618,9 +641,10 @@ func formatUptime(d time.Duration) string {
618641
return "unknown"
619642
}
620643

621-
days := int(d.Hours() / 24)
622-
hours := int(d.Hours()) % 60
623-
minutes := int(d.Minutes()) % 60
644+
totalMinutes := int(d.Minutes())
645+
days := totalMinutes / (60 * 24)
646+
hours := (totalMinutes / 60) % 24
647+
minutes := totalMinutes % 60
624648

625649
if days > 0 {
626650
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)

0 commit comments

Comments
 (0)