Merge remote-tracking branch 'rephorm/filter'

This commit is contained in:
Sean E. Russell 2020-02-14 09:35:58 -06:00
commit f850a47d91
12 changed files with 266 additions and 11 deletions

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 {

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

113
termui/entry.go Normal file
View File

@ -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)
}

View File

@ -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

24
utils/runes.go Normal file
View File

@ -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
utils/runes_test.go Normal file
View File

@ -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"},
{"⦅fullwidth⦆", 15, ".", "⦅fullwidth⦆"},
{"⦅fullwidth⦆", 14, ".", ".fullwidth⦆"},
{"⦅fullwidth⦆", 13, ".", ".ullwidth⦆"},
{"⦅fullwidth⦆", 10, ".", ".width⦆"},
{"⦅fullwidth⦆", 9, ".", ".width⦆"},
{"⦅fullwidth⦆", 8, ".", ".width⦆"},
{"⦅fullwidth⦆", 3, ".", ".⦆"},
{"⦅fullwidth⦆", 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)
}
}
}

View File

@ -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

View File

@ -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)