Skip to content
This repository has been archived by the owner on Aug 29, 2020. It is now read-only.

Add support for filtering processes by substring. #147

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ snap connect gotop-cjbassi:system-observe
### Keybinds

- Quit: `q` or `<C-c>`
- Process navigation
- Process navigation:
- `k` and `<Up>`: up
- `j` and `<Down`: down
- `j` and `<Down>`: down
- `<C-u>`: half page up
- `<C-d>`: half page down
- `<C-b>`: full page up
Expand All @@ -83,10 +83,15 @@ snap connect gotop-cjbassi:system-observe
- Process actions:
- `<Tab>`: toggle process grouping
- `dd`: kill selected process or group of processes
- Process sorting
- Process sorting:
- `c`: CPU
- `m`: Mem
- `p`: PID
- Process filtering:
- `/`: start editing filter
- (while editing):
- `<Enter>` accept filter
- `<C-c>` and `<Escape>`: clear filter
- CPU and Mem graph scaling:
- `h`: scale in
- `l`: scale out
Expand Down
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ func eventLoop() {
}
}
case e := <-uiEvents:
if proc.HandleEvent(e) {
ui.Render(proc)
break
}
switch e.ID {
case "q", "<C-c>":
return
Expand Down Expand Up @@ -389,6 +393,9 @@ func eventLoop() {
case "m", "c", "p":
proc.ChangeProcSortMethod(w.ProcSortMethod(e.ID))
ui.Render(proc)
case "/":
proc.SetEditingFilter(true)
ui.Render(proc)
}

if previousKey == e.ID {
Expand Down
113 changes: 113 additions & 0 deletions src/termui/entry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package termui

import (
"image"
"strings"
"unicode/utf8"

"github.com/cjbassi/gotop/src/utils"
. "github.com/gizak/termui/v3"
rw "github.com/mattn/go-runewidth"
)

const (
ELLIPSIS = "…"
CURSOR = " "
)

type Entry struct {
Block

Style Style

Label string
Value string
ShowWhenEmpty bool
UpdateCallback func(string)

editing bool
}

func (self *Entry) SetEditing(editing bool) {
self.editing = editing
}

func (self *Entry) update() {
if self.UpdateCallback != nil {
self.UpdateCallback(self.Value)
}
}

// HandleEvent handles input events if the entry is being edited.
// Returns true if the event was handled.
func (self *Entry) HandleEvent(e Event) bool {
if !self.editing {
return false
}
if utf8.RuneCountInString(e.ID) == 1 {
self.Value += e.ID
self.update()
return true
}
switch e.ID {
case "<C-c>", "<Escape>":
self.Value = ""
self.editing = false
self.update()
case "<Enter>":
self.editing = false
case "<Backspace>":
if self.Value != "" {
r := []rune(self.Value)
self.Value = string(r[:len(r)-1])
self.update()
}
case "<Space>":
self.Value += " "
self.update()
default:
return false
}
return true
}

func (self *Entry) Draw(buf *Buffer) {
if self.Value == "" && !self.editing && !self.ShowWhenEmpty {
return
}

style := self.Style
label := self.Label
if self.editing {
label += "["
style = NewStyle(style.Fg, style.Bg, ModifierBold)
}
cursorStyle := NewStyle(style.Bg, style.Fg, ModifierClear)

p := image.Pt(self.Min.X, self.Min.Y)
buf.SetString(label, style, p)
p.X += rw.StringWidth(label)

tail := " "
if self.editing {
tail = "] "
}

maxLen := self.Max.X - p.X - rw.StringWidth(tail)
if self.editing {
maxLen -= 1 // for cursor
}
value := utils.TruncateFront(self.Value, maxLen, ELLIPSIS)
buf.SetString(value, self.Style, p)
p.X += rw.StringWidth(value)

if self.editing {
buf.SetString(CURSOR, cursorStyle, p)
p.X += rw.StringWidth(CURSOR)
if remaining := maxLen - rw.StringWidth(value); remaining > 0 {
buf.SetString(strings.Repeat(" ", remaining), self.TitleStyle, p)
p.X += remaining
}
}
buf.SetString(tail, style, p)
}
3 changes: 3 additions & 0 deletions src/termui/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ func (self *Table) Draw(buf *Buffer) {
func (self *Table) drawLocation(buf *Buffer) {
total := len(self.Rows)
topRow := self.TopRow + 1
if topRow > total {
topRow = total
}
bottomRow := self.TopRow + self.Inner.Dy() - 1
if bottomRow > total {
bottomRow = total
Expand Down
24 changes: 24 additions & 0 deletions src/utils/runes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package utils

import (
rw "github.com/mattn/go-runewidth"
)

func TruncateFront(s string, w int, prefix string) string {
if rw.StringWidth(s) <= w {
return s
}
r := []rune(s)
pw := rw.StringWidth(prefix)
w -= pw
width := 0
i := len(r) - 1
for ; i >= 0; i-- {
cw := rw.RuneWidth(r[i])
width += cw
if width > w {
break
}
}
return prefix + string(r[i+1:len(r)])
}
50 changes: 50 additions & 0 deletions src/utils/runes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package utils

import "testing"

const (
ELLIPSIS = "…"
)

func TestTruncateFront(t *testing.T) {
tests := []struct {
s string
w int
prefix string
want string
}{
{"", 0, ELLIPSIS, ""},
{"", 1, ELLIPSIS, ""},
{"", 10, ELLIPSIS, ""},

{"abcdef", 0, ELLIPSIS, ELLIPSIS},
{"abcdef", 1, ELLIPSIS, ELLIPSIS},
{"abcdef", 2, ELLIPSIS, ELLIPSIS + "f"},
{"abcdef", 5, ELLIPSIS, ELLIPSIS + "cdef"},
{"abcdef", 6, ELLIPSIS, "abcdef"},
{"abcdef", 10, ELLIPSIS, "abcdef"},

{"abcdef", 0, "...", "..."},
{"abcdef", 1, "...", "..."},
{"abcdef", 3, "...", "..."},
{"abcdef", 4, "...", "...f"},
{"abcdef", 5, "...", "...ef"},
{"abcdef", 6, "...", "abcdef"},
{"abcdef", 10, "...", "abcdef"},

{"⦅full~width⦆", 15, ".", "⦅full~width⦆"},
{"⦅full~width⦆", 14, ".", ".full~width⦆"},
{"⦅full~width⦆", 13, ".", ".ull~width⦆"},
{"⦅full~width⦆", 10, ".", ".~width⦆"},
{"⦅full~width⦆", 9, ".", ".width⦆"},
{"⦅full~width⦆", 8, ".", ".width⦆"},
{"⦅full~width⦆", 3, ".", ".⦆"},
{"⦅full~width⦆", 2, ".", "."},
}

for _, test := range tests {
if got := TruncateFront(test.s, test.w, test.prefix); got != test.want {
t.Errorf("TruncateFront(%q, %d, %q) = %q; want %q", test.s, test.w, test.prefix, got, test.want)
}
}
}
12 changes: 9 additions & 3 deletions src/widgets/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
const KEYBINDS = `
Quit: q or <C-c>

Process navigation
Process navigation:
- k and <Up>: up
- j and <Down>: down
- <C-u>: half page up
Expand All @@ -24,11 +24,17 @@ Process actions:
- <Tab>: toggle process grouping
- dd: kill selected process or group of processes

Process sorting
Process sorting:
- c: CPU
- m: Mem
- p: PID

Process filtering:
- /: start editing filter
- (while editing):
- <Enter>: accept filter
- <C-c> and <Escape>: clear filter

CPU and Mem graph scaling:
- h: scale in
- l: scale out
Expand All @@ -46,7 +52,7 @@ func NewHelpMenu() *HelpMenu {

func (self *HelpMenu) Resize(termWidth, termHeight int) {
textWidth := 53
textHeight := 22
textHeight := strings.Count(KEYBINDS, "\n") + 1
x := (termWidth - textWidth) / 2
y := (termHeight - textHeight) / 2

Expand Down
46 changes: 46 additions & 0 deletions src/widgets/proc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import (
"os/exec"
"sort"
"strconv"
"strings"
"time"

psCPU "github.com/shirou/gopsutil/cpu"

ui "github.com/cjbassi/gotop/src/termui"
"github.com/cjbassi/gotop/src/utils"
tui "github.com/gizak/termui/v3"
)

const (
Expand All @@ -37,9 +39,11 @@ type Proc struct {

type ProcWidget struct {
*ui.Table
entry *ui.Entry
cpuCount int
updateInterval time.Duration
sortMethod ProcSortMethod
filter string
groupedProcs []Proc
ungroupedProcs []Proc
showGroupedProcs bool
Expand All @@ -56,6 +60,16 @@ func NewProcWidget() *ProcWidget {
cpuCount: cpuCount,
sortMethod: ProcSortCpu,
showGroupedProcs: true,
filter: "",
}
self.entry = &ui.Entry{
Style: self.TitleStyle,
Label: " Filter: ",
Value: "",
UpdateCallback: func(val string) {
self.filter = val
self.update()
},
}
self.Title = " Processes "
self.ShowCursor = true
Expand Down Expand Up @@ -86,6 +100,37 @@ func NewProcWidget() *ProcWidget {
return self
}

func (self *ProcWidget) SetEditingFilter(editing bool) {
self.entry.SetEditing(editing)
}

func (self *ProcWidget) HandleEvent(e tui.Event) bool {
return self.entry.HandleEvent(e)
}

func (self *ProcWidget) SetRect(x1, y1, x2, y2 int) {
self.Table.SetRect(x1, y1, x2, y2)
self.entry.SetRect(x1+2, y2-1, x2-2, y2)
}

func (self *ProcWidget) Draw(buf *tui.Buffer) {
self.Table.Draw(buf)
self.entry.Draw(buf)
}

func (self *ProcWidget) filterProcs(procs []Proc) []Proc {
if self.filter == "" {
return procs
}
var filtered []Proc
for _, proc := range procs {
if strings.Contains(proc.FullCommand, self.filter) || strings.Contains(fmt.Sprintf("%d", proc.Pid), self.filter) {
filtered = append(filtered, proc)
}
}
return filtered
}

func (self *ProcWidget) update() {
procs, err := getProcs()
if err != nil {
Expand All @@ -98,6 +143,7 @@ func (self *ProcWidget) update() {
procs[i].Cpu /= float64(self.cpuCount)
}

procs = self.filterProcs(procs)
self.ungroupedProcs = procs
self.groupedProcs = groupProcs(procs)

Expand Down