From 866281cc42ab557c71a03f81fdf43f8b70c8ec7d Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Fri, 31 May 2019 19:58:29 -0700 Subject: [PATCH] First pass at filter support. Press / to input a filter string. The proc list will be filtered to procs that contain the filter as a substring of either the command or the string representation of the PID. --- README.md | 9 ++++-- main.go | 62 +++++++++++++++++++++++++++---------- src/widgets/help.go | 12 ++++++-- src/widgets/proc.go | 75 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 458d534..3c060ac 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ snap connect gotop-cjbassi:system-observe ### Keybinds - Quit: `q` or `` -- Process navigation +- Process navigation: - `k` and ``: up - `j` and ``: half page up @@ -83,10 +83,15 @@ snap connect gotop-cjbassi:system-observe - Process actions: - ``: 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): + - accept filter + - : clear filter - CPU and Mem graph scaling: - `h`: scale in - `l`: scale out diff --git a/main.go b/main.go index 4ed0231..2eb653c 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "strconv" "syscall" "time" + "unicode/utf8" docopt "github.com/docopt/docopt.go" ui "github.com/gizak/termui/v3" @@ -296,12 +297,8 @@ func eventLoop() { } } case e := <-uiEvents: - switch e.ID { - case "q", "": - return - case "?": - helpVisible = !helpVisible - case "": + // Handle resize event always. + if e.ID == "" { payload := e.Payload.(ui.Resize) termWidth, termHeight := payload.Width, payload.Height if statusbar { @@ -312,23 +309,55 @@ func eventLoop() { } help.Resize(payload.Width, payload.Height) ui.Clear() + + if helpVisible { + ui.Render(help) + } else { + ui.Render(grid) + if statusbar { + ui.Render(bar) + } + } } - if helpVisible { + if proc.EditingFilter() { + if utf8.RuneCountInString(e.ID) == 1 { + proc.SetFilter(proc.Filter() + e.ID) + ui.Render(proc) + } switch e.ID { + case "": + proc.SetFilter("") + proc.SetEditingFilter(false) + ui.Render(proc) + case "": + proc.SetEditingFilter(false) + ui.Render(proc) + case "": + if filter := proc.Filter(); filter != "" { + proc.SetFilter(filter[:len(filter)-1]) + } + ui.Render(proc) + } + } else if helpVisible { + switch e.ID { + case "q", "": + return case "?": - ui.Clear() - ui.Render(help) + helpVisible = false + ui.Render(grid) case "": helpVisible = false ui.Render(grid) - case "": - ui.Render(help) } } else { switch e.ID { + case "q", "": + return case "?": - ui.Render(grid) + helpVisible = true + ui.Clear() + ui.Render(help) case "h": graphHorizontalScale += graphHorizontalScaleDelta cpu.HorizontalScale = graphHorizontalScale @@ -341,11 +370,6 @@ func eventLoop() { mem.HorizontalScale = graphHorizontalScale ui.Render(cpu, mem) } - case "": - ui.Render(grid) - if statusbar { - ui.Render(bar) - } case "": payload := e.Payload.(ui.Mouse) proc.HandleClick(payload.X, payload.Y) @@ -389,6 +413,10 @@ func eventLoop() { case "m", "c", "p": proc.ChangeProcSortMethod(w.ProcSortMethod(e.ID)) ui.Render(proc) + case "/": + proc.SetFilter("") + proc.SetEditingFilter(true) + ui.Render(proc) } if previousKey == e.ID { diff --git a/src/widgets/help.go b/src/widgets/help.go index eaa6f59..47b3c14 100644 --- a/src/widgets/help.go +++ b/src/widgets/help.go @@ -10,7 +10,7 @@ import ( const KEYBINDS = ` Quit: q or -Process navigation +Process navigation: - k and : up - j and : down - : half page up @@ -24,11 +24,17 @@ Process actions: - : 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): + - : accept filter + - clear filter + CPU and Mem graph scaling: - h: scale in - l: scale out @@ -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 diff --git a/src/widgets/proc.go b/src/widgets/proc.go index d48ed9a..d94a5bf 100644 --- a/src/widgets/proc.go +++ b/src/widgets/proc.go @@ -2,21 +2,26 @@ package widgets import ( "fmt" + "image" "log" "os/exec" "sort" "strconv" + "strings" "time" + "unicode/utf8" 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 ( UP_ARROW = "▲" DOWN_ARROW = "▼" + ELLIPSIS = "…" ) type ProcSortMethod string @@ -40,6 +45,8 @@ type ProcWidget struct { cpuCount int updateInterval time.Duration sortMethod ProcSortMethod + filter string + editingFilter bool groupedProcs []Proc ungroupedProcs []Proc showGroupedProcs bool @@ -56,6 +63,8 @@ func NewProcWidget() *ProcWidget { cpuCount: cpuCount, sortMethod: ProcSortCpu, showGroupedProcs: true, + filter: "", + editingFilter: false, } self.Title = " Processes " self.ShowCursor = true @@ -86,6 +95,71 @@ func NewProcWidget() *ProcWidget { return self } +func (self *ProcWidget) Filter() string { + return self.filter +} + +func (self *ProcWidget) SetFilter(filter string) { + self.filter = filter +} + +func (self *ProcWidget) EditingFilter() bool { + return self.editingFilter +} + +func (self *ProcWidget) SetEditingFilter(editing bool) { + self.editingFilter = editing + if !editing { + self.update() + } +} + +func (self *ProcWidget) Draw(buf *tui.Buffer) { + self.Table.Draw(buf) + if self.filter != "" || self.editingFilter { + self.drawFilter(buf) + } +} + +func (self *ProcWidget) drawFilter(buf *tui.Buffer) { + style := self.TitleStyle + label := "Filter: " + if self.editingFilter { + label = "[ Filter: " + style = tui.NewStyle(style.Fg, style.Bg, tui.ModifierBold) + } + + p := image.Pt(self.Min.X+2, self.Max.Y-1) + buf.SetString(label, style, p) + p.X += utf8.RuneCountInString(label) + + maxLen := self.Max.X - p.X - 4 + filter := self.filter + if l := utf8.RuneCountInString(filter); l > maxLen { + filter = ELLIPSIS + filter[l-maxLen+1:] + } + buf.SetString(filter, self.TitleStyle, p) + p.X += utf8.RuneCountInString(filter) + + if self.editingFilter { + remaining := self.Max.X - 2 - p.X + buf.SetString(fmt.Sprintf("%*s", remaining, "]"), style, p) + } +} + +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 { @@ -98,6 +172,7 @@ func (self *ProcWidget) update() { procs[i].Cpu /= float64(self.cpuCount) } + procs = self.filterProcs(procs) self.ungroupedProcs = procs self.groupedProcs = groupProcs(procs)