summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSean E. Russell <ser@ser1.net>2020-02-14 09:35:58 -0600
committerSean E. Russell <ser@ser1.net>2020-02-14 09:35:58 -0600
commitf850a47d91d413de40de219aa597ade41a05f757 (patch)
treefa15d4e8ddc62888788d6814e4ab3c010931d892
parent64d4a81212a43a4b805b92963fd4da230b3ca313 (diff)
parent246ebfbff2c87f66218c42ea9490214006f2efc8 (diff)
Merge remote-tracking branch 'rephorm/filter'
-rw-r--r--CHANGELOG.md1
-rw-r--r--README.md9
-rw-r--r--cmd/gotop/main.go9
-rw-r--r--config.go1
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--termui/entry.go113
-rw-r--r--termui/table.go3
-rw-r--r--utils/runes.go24
-rw-r--r--utils/runes_test.go50
-rw-r--r--widgets/help.go18
-rw-r--r--widgets/proc.go46
12 files changed, 266 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28af61d..cf1f46a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ Bug fixes & pull requests
- Merged pull request for README clean-ups (theverything:add-missing-option-to-readme)
- Merge Nord color scheme (jrswab:nordColorScheme)
- Merge support for multiple (and filtering) network interfaces (mattLLVW:feature/network_interface_list)
+- Merge filtering subprocesses by substring (rephorm:filter)
## [3.1.0] - 2020-02-13
diff --git a/README.md b/README.md
index 6f1061c..0903624 100644
--- a/README.md
+++ b/README.md
@@ -37,9 +37,9 @@ Unzip it and then move `gotop` into your `$PATH` somewhere. If you're on a Debi
### 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
@@ -55,6 +55,11 @@ Unzip it and then move `gotop` into your `$PATH` somewhere. If you're on a Debi
- `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
diff --git a/cmd/gotop/main.go b/cmd/gotop/main.go
index bf9b72d..d3384b0 100644
--- a/cmd/gotop/main.go
+++ b/cmd/gotop/main.go
@@ -221,6 +221,10 @@ func eventLoop(c gotop.Config, grid *layout.MyGrid) {
}
}
case e := <-uiEvents:
+ if grid.Proc != nil && grid.Proc.HandleEvent(e) {
+ ui.Render(grid.Proc)
+ break
+ }
switch e.ID {
case "q", "<C-c>":
return
@@ -354,6 +358,11 @@ func eventLoop(c gotop.Config, grid *layout.MyGrid) {
grid.Proc.ChangeProcSortMethod(w.ProcSortMethod(e.ID))
ui.Render(grid.Proc)
}
+ case "/":
+ if grid.Proc != nil {
+ grid.Proc.SetEditingFilter(true)
+ ui.Render(grid.Proc)
+ }
}
if previousKey == e.ID {
diff --git a/config.go b/config.go
index 7eff495..f6cf4a5 100644
--- a/config.go
+++ b/config.go
@@ -14,7 +14,6 @@ import (
// TODO: Merge #167 configuration file (jrswab:configFile111)
// TODO: Merge #157 FreeBSD fixes & Nvidia GPU support (kraust:master)
// TODO: Merge #156 Added temperatures for NVidia GPUs (azak-azkaran:master)
-// TODO: Merge #147 filtering subprocesses by substring (rephorm:filter)
// TODO: Merge #140 color-related fix (Tazer:master)
// TODO: Merge #135 linux console font (cmatsuoka:console-font)
type Config struct {
diff --git a/go.mod b/go.mod
index e075085..3ccbb21 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
github.com/docopt/docopt.go v0.0.0-20180111231733-ee0de3bc6815
github.com/gizak/termui/v3 v3.0.0
github.com/go-ole/go-ole v1.2.4 // indirect
+ github.com/mattn/go-runewidth v0.0.4
github.com/shirou/gopsutil v2.18.11+incompatible
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
github.com/stretchr/testify v1.4.0
diff --git a/go.sum b/go.sum
index 91a8c8b..b05f282 100644
--- a/go.sum
+++ b/go.sum
@@ -20,6 +20,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
diff --git a/termui/entry.go b/termui/entry.go
new file mode 100644
index 0000000..2ba4ecc
--- /dev/null
+++ b/termui/entry.go
@@ -0,0 +1,113 @@
+package termui
+
+import (
+ "image"
+ "strings"
+ "unicode/utf8"
+
+ . "github.com/gizak/termui/v3"
+ rw "github.com/mattn/go-runewidth"
+ "github.com/xxxserxxx/gotop/utils"
+)
+
+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)
+}
diff --git a/termui/table.go b/termui/table.go
index eeb82af..e587839 100644
--- a/termui/table.go
+++ b/termui/table.go
@@ -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
diff --git a/utils/runes.go b/utils/runes.go
new file mode 100644
index 0000000..76cb5e3
--- /dev/null
+++ b/utils/runes.go
@@ -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)])
+}
diff --git a/utils/runes_test.go b/utils/runes_test.go
new file mode 100644
index 0000000..67ceefc
--- /dev/null
+++ b/utils/runes_test.go
@@ -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)
+ }
+ }
+}
diff --git a/widgets/help.go b/widgets/help.go
index 953de45..9f78dd0 100644
--- a/widgets/help.go
+++ b/widgets/help.go
@@ -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
@@ -26,11 +26,17 @@ Process actions:
- d3: kill selected process or group of processes with SIGQUIT (3)
- d9: kill selected process or group of processes with SIGKILL (9)
-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
@@ -47,12 +53,8 @@ func NewHelpMenu() *HelpMenu {
}
func (self *HelpMenu) Resize(termWidth, termHeight int) {
- var textWidth = 0
- for _, line := range strings.Split(KEYBINDS, "\n") {
- textWidth = maxInt(len(line), textWidth)
- }
- textWidth += 2
- textHeight := 28
+ textWidth := 53
+ textHeight := strings.Count(KEYBINDS, "\n") + 1
x := (termWidth - textWidth) / 2
y := (termHeight - textHeight) / 2
diff --git a/widgets/proc.go b/widgets/proc.go
index 58ad6dd..9fed067 100644
--- a/widgets/proc.go
+++ b/widgets/proc.go
@@ -6,10 +6,12 @@ import (
"os/exec"
"sort"
"strconv"
+ "strings"
"time"
psCPU "github.com/shirou/gopsutil/cpu"
+ tui "github.com/gizak/termui/v3"
ui "github.com/xxxserxxx/gotop/termui"
"github.com/xxxserxxx/gotop/utils"
)
@@ -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
@@ -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
@@ -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 {
@@ -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)