From 866281cc42ab557c71a03f81fdf43f8b70c8ec7d Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Fri, 31 May 2019 19:58:29 -0700 Subject: [PATCH 01/13] 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) From 5941bec6c13e802336ec51bb25024fa75a087dcf Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Mon, 3 Jun 2019 14:08:57 -0700 Subject: [PATCH 02/13] Update process list immediately when filter changes. --- src/widgets/proc.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widgets/proc.go b/src/widgets/proc.go index d94a5bf..fa1b900 100644 --- a/src/widgets/proc.go +++ b/src/widgets/proc.go @@ -101,6 +101,7 @@ func (self *ProcWidget) Filter() string { func (self *ProcWidget) SetFilter(filter string) { self.filter = filter + self.update() } func (self *ProcWidget) EditingFilter() bool { From 2aea13bf0a89e9719d40e6ddb200b89982f18ebd Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Mon, 3 Jun 2019 14:13:31 -0700 Subject: [PATCH 03/13] Refactor filter editing input handling. Only swallow events that are explicitly handled by the editor. Fall through to the default event handling for all other events. Also, cancel editing on and fix README file formatting. --- README.md | 8 ++-- main.go | 89 ++++++++++++++++++++++++--------------------- src/widgets/help.go | 2 +- 3 files changed, 53 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 3c060ac..444d3ad 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ snap connect gotop-cjbassi:system-observe - Quit: `q` or `` - Process navigation: - `k` and ``: up - - `j` and ``: down - ``: half page up - ``: half page down - ``: full page up @@ -88,10 +88,10 @@ snap connect gotop-cjbassi:system-observe - `m`: Mem - `p`: PID - Process filtering: - - /: start editing filter + - `/`: start editing filter - (while editing): - - accept filter - - : clear filter + - `` accept filter + - `` and ``: clear filter - CPU and Mem graph scaling: - `h`: scale in - `l`: scale out diff --git a/main.go b/main.go index 2eb653c..48f750c 100644 --- a/main.go +++ b/main.go @@ -274,6 +274,33 @@ func initWidgets() { } } +// handleEditFilterEvents handles events while editing the proc filter. +// Returns true if the event was handled. +func handleEditFilterEvents(e ui.Event) bool { + if utf8.RuneCountInString(e.ID) == 1 { + proc.SetFilter(proc.Filter() + e.ID) + ui.Render(proc) + return true + } + 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) + default: + return false + } + return true +} + func eventLoop() { drawTicker := time.NewTicker(updateInterval).C @@ -297,8 +324,16 @@ func eventLoop() { } } case e := <-uiEvents: - // Handle resize event always. - if e.ID == "" { + + if proc.EditingFilter() && handleEditFilterEvents(e) { + break + } + switch e.ID { + case "q", "": + return + case "?": + helpVisible = !helpVisible + case "": payload := e.Payload.(ui.Resize) termWidth, termHeight := payload.Width, payload.Height if statusbar { @@ -309,55 +344,23 @@ 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 proc.EditingFilter() { - if utf8.RuneCountInString(e.ID) == 1 { - proc.SetFilter(proc.Filter() + e.ID) - ui.Render(proc) - } + if helpVisible { 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 "?": - helpVisible = false - ui.Render(grid) + ui.Clear() + ui.Render(help) case "": helpVisible = false ui.Render(grid) + case "": + ui.Render(help) } } else { switch e.ID { - case "q", "": - return case "?": - helpVisible = true - ui.Clear() - ui.Render(help) + ui.Render(grid) case "h": graphHorizontalScale += graphHorizontalScaleDelta cpu.HorizontalScale = graphHorizontalScale @@ -370,6 +373,11 @@ 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) @@ -414,7 +422,6 @@ func eventLoop() { proc.ChangeProcSortMethod(w.ProcSortMethod(e.ID)) ui.Render(proc) case "/": - proc.SetFilter("") proc.SetEditingFilter(true) ui.Render(proc) } diff --git a/src/widgets/help.go b/src/widgets/help.go index 47b3c14..97cfbd6 100644 --- a/src/widgets/help.go +++ b/src/widgets/help.go @@ -33,7 +33,7 @@ Process filtering: - /: start editing filter - (while editing): - : accept filter - - clear filter + - and : clear filter CPU and Mem graph scaling: - h: scale in From bb44c222ef68b8b81bb0b8ec0322c2a47bc38db9 Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Mon, 3 Jun 2019 15:14:22 -0700 Subject: [PATCH 04/13] Add cursor while editing filter. Also, change from [Filter: ...] to Filter: [...]. --- src/widgets/proc.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/widgets/proc.go b/src/widgets/proc.go index fa1b900..688288b 100644 --- a/src/widgets/proc.go +++ b/src/widgets/proc.go @@ -22,6 +22,7 @@ const ( UP_ARROW = "▲" DOWN_ARROW = "▼" ELLIPSIS = "…" + CURSOR = " " ) type ProcSortMethod string @@ -126,9 +127,10 @@ func (self *ProcWidget) drawFilter(buf *tui.Buffer) { style := self.TitleStyle label := "Filter: " if self.editingFilter { - label = "[ Filter: " + label = "Filter: [" style = tui.NewStyle(style.Fg, style.Bg, tui.ModifierBold) } + cursorStyle := tui.NewStyle(style.Bg, style.Fg, tui.ModifierClear) p := image.Pt(self.Min.X+2, self.Max.Y-1) buf.SetString(label, style, p) @@ -143,6 +145,8 @@ func (self *ProcWidget) drawFilter(buf *tui.Buffer) { p.X += utf8.RuneCountInString(filter) if self.editingFilter { + buf.SetString(CURSOR, cursorStyle, p) + p.X += 1 remaining := self.Max.X - 2 - p.X buf.SetString(fmt.Sprintf("%*s", remaining, "]"), style, p) } From 6d0eae1f0b9d2d74e4ada27265d309a3c447b681 Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Mon, 3 Jun 2019 15:18:40 -0700 Subject: [PATCH 05/13] Fix counter when list has 0 items. --- src/termui/table.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/termui/table.go b/src/termui/table.go index eeb82af..e587839 100644 --- a/src/termui/table.go +++ b/src/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 From 8549ae34a10b17297bc8b19faae03da265ed9731 Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Mon, 3 Jun 2019 15:30:10 -0700 Subject: [PATCH 06/13] Add padding around filter label and edit box. --- src/widgets/proc.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/widgets/proc.go b/src/widgets/proc.go index 688288b..df6d7e1 100644 --- a/src/widgets/proc.go +++ b/src/widgets/proc.go @@ -125,9 +125,9 @@ func (self *ProcWidget) Draw(buf *tui.Buffer) { func (self *ProcWidget) drawFilter(buf *tui.Buffer) { style := self.TitleStyle - label := "Filter: " + label := " Filter: " if self.editingFilter { - label = "Filter: [" + label += "[" style = tui.NewStyle(style.Fg, style.Bg, tui.ModifierBold) } cursorStyle := tui.NewStyle(style.Bg, style.Fg, tui.ModifierClear) @@ -136,7 +136,7 @@ func (self *ProcWidget) drawFilter(buf *tui.Buffer) { buf.SetString(label, style, p) p.X += utf8.RuneCountInString(label) - maxLen := self.Max.X - p.X - 4 + maxLen := self.Max.X - p.X - 5 filter := self.filter if l := utf8.RuneCountInString(filter); l > maxLen { filter = ELLIPSIS + filter[l-maxLen+1:] @@ -148,7 +148,7 @@ func (self *ProcWidget) drawFilter(buf *tui.Buffer) { buf.SetString(CURSOR, cursorStyle, p) p.X += 1 remaining := self.Max.X - 2 - p.X - buf.SetString(fmt.Sprintf("%*s", remaining, "]"), style, p) + buf.SetString(fmt.Sprintf("%*s", remaining, "] "), style, p) } } From 38c92e910d0f2fd4150b08113ffbd5580a075ffa Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Mon, 3 Jun 2019 15:33:42 -0700 Subject: [PATCH 07/13] Handle space in filter input box. --- main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main.go b/main.go index 48f750c..1e677a7 100644 --- a/main.go +++ b/main.go @@ -295,6 +295,9 @@ func handleEditFilterEvents(e ui.Event) bool { proc.SetFilter(filter[:len(filter)-1]) } ui.Render(proc) + case "": + proc.SetFilter(proc.Filter() + " ") + ui.Render(proc) default: return false } From 32f88622c721fdea1fa93e49791c15103ac202ec Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Mon, 3 Jun 2019 15:36:01 -0700 Subject: [PATCH 08/13] Convert tabs to spaces in README.md. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 444d3ad..b34b080 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,9 @@ snap connect gotop-cjbassi:system-observe - `p`: PID - Process filtering: - `/`: start editing filter - - (while editing): - - `` accept filter - - `` and ``: clear filter + - (while editing): + - `` accept filter + - `` and ``: clear filter - CPU and Mem graph scaling: - `h`: scale in - `l`: scale out From f3b451c887ed8c9d50c750e6c4cc603cd87a0ccd Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Mon, 3 Jun 2019 16:02:22 -0700 Subject: [PATCH 09/13] Move filter event handling into proc widget. --- main.go | 35 ++--------------------------------- src/widgets/proc.go | 45 ++++++++++++++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 46 deletions(-) diff --git a/main.go b/main.go index 1e677a7..3cf4add 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,6 @@ import ( "strconv" "syscall" "time" - "unicode/utf8" docopt "github.com/docopt/docopt.go" ui "github.com/gizak/termui/v3" @@ -274,36 +273,6 @@ func initWidgets() { } } -// handleEditFilterEvents handles events while editing the proc filter. -// Returns true if the event was handled. -func handleEditFilterEvents(e ui.Event) bool { - if utf8.RuneCountInString(e.ID) == 1 { - proc.SetFilter(proc.Filter() + e.ID) - ui.Render(proc) - return true - } - 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) - case "": - proc.SetFilter(proc.Filter() + " ") - ui.Render(proc) - default: - return false - } - return true -} - func eventLoop() { drawTicker := time.NewTicker(updateInterval).C @@ -327,8 +296,8 @@ func eventLoop() { } } case e := <-uiEvents: - - if proc.EditingFilter() && handleEditFilterEvents(e) { + if proc.HandleEvent(e) { + ui.Render(proc) break } switch e.ID { diff --git a/src/widgets/proc.go b/src/widgets/proc.go index df6d7e1..3a1c61e 100644 --- a/src/widgets/proc.go +++ b/src/widgets/proc.go @@ -96,19 +96,6 @@ func NewProcWidget() *ProcWidget { return self } -func (self *ProcWidget) Filter() string { - return self.filter -} - -func (self *ProcWidget) SetFilter(filter string) { - self.filter = filter - self.update() -} - -func (self *ProcWidget) EditingFilter() bool { - return self.editingFilter -} - func (self *ProcWidget) SetEditingFilter(editing bool) { self.editingFilter = editing if !editing { @@ -116,6 +103,38 @@ func (self *ProcWidget) SetEditingFilter(editing bool) { } } +// handleEditFilterEvents handles events while editing the proc filter. +// Returns true if the event was handled. +func (self *ProcWidget) HandleEvent(e tui.Event) bool { + if !self.editingFilter { + return false + } + if utf8.RuneCountInString(e.ID) == 1 { + self.filter += e.ID + self.update() + return true + } + switch e.ID { + case "", "": + self.filter = "" + self.update() + self.SetEditingFilter(false) + case "": + self.SetEditingFilter(false) + case "": + if self.filter != "" { + self.filter = self.filter[:len(self.filter)-1] + self.update() + } + case "": + self.filter += " " + self.update() + default: + return false + } + return true +} + func (self *ProcWidget) Draw(buf *tui.Buffer) { self.Table.Draw(buf) if self.filter != "" || self.editingFilter { From 84a4bbd440f665a42d8192a8da21427c672032ec Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Tue, 4 Jun 2019 14:21:13 -0700 Subject: [PATCH 10/13] Fix fullwidth rune handling. --- src/utils/runes.go | 24 ++++++++++++++++++++ src/utils/runes_test.go | 50 +++++++++++++++++++++++++++++++++++++++++ src/widgets/proc.go | 36 ++++++++++++++++++++--------- 3 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 src/utils/runes.go create mode 100644 src/utils/runes_test.go diff --git a/src/utils/runes.go b/src/utils/runes.go new file mode 100644 index 0000000..76cb5e3 --- /dev/null +++ b/src/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/src/utils/runes_test.go b/src/utils/runes_test.go new file mode 100644 index 0000000..67ceefc --- /dev/null +++ b/src/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/src/widgets/proc.go b/src/widgets/proc.go index 3a1c61e..a598498 100644 --- a/src/widgets/proc.go +++ b/src/widgets/proc.go @@ -16,6 +16,7 @@ import ( ui "github.com/cjbassi/gotop/src/termui" "github.com/cjbassi/gotop/src/utils" tui "github.com/gizak/termui/v3" + rw "github.com/mattn/go-runewidth" ) const ( @@ -123,7 +124,8 @@ func (self *ProcWidget) HandleEvent(e tui.Event) bool { self.SetEditingFilter(false) case "": if self.filter != "" { - self.filter = self.filter[:len(self.filter)-1] + r := []rune(self.filter) + self.filter = string(r[:len(r)-1]) self.update() } case "": @@ -143,6 +145,8 @@ func (self *ProcWidget) Draw(buf *tui.Buffer) { } func (self *ProcWidget) drawFilter(buf *tui.Buffer) { + padding := 2 + style := self.TitleStyle label := " Filter: " if self.editingFilter { @@ -151,24 +155,34 @@ func (self *ProcWidget) drawFilter(buf *tui.Buffer) { } cursorStyle := tui.NewStyle(style.Bg, style.Fg, tui.ModifierClear) - p := image.Pt(self.Min.X+2, self.Max.Y-1) + p := image.Pt(self.Min.X+padding, self.Max.Y-1) buf.SetString(label, style, p) - p.X += utf8.RuneCountInString(label) + p.X += rw.StringWidth(label) - maxLen := self.Max.X - p.X - 5 - filter := self.filter - if l := utf8.RuneCountInString(filter); l > maxLen { - filter = ELLIPSIS + filter[l-maxLen+1:] + tail := " " + if self.editingFilter { + tail = "] " } + + maxLen := self.Max.X - p.X - padding - rw.StringWidth(tail) + if self.editingFilter { + maxLen -= 1 // for cursor + } + + filter := utils.TruncateFront(self.filter, maxLen, ELLIPSIS) buf.SetString(filter, self.TitleStyle, p) - p.X += utf8.RuneCountInString(filter) + p.X += rw.StringWidth(filter) if self.editingFilter { buf.SetString(CURSOR, cursorStyle, p) - p.X += 1 - remaining := self.Max.X - 2 - p.X - buf.SetString(fmt.Sprintf("%*s", remaining, "] "), style, p) + p.X += rw.StringWidth(CURSOR) + + if remaining := maxLen - rw.StringWidth(filter); remaining > 0 { + buf.SetString(strings.Repeat(" ", remaining), self.TitleStyle, p) + p.X += remaining + } } + buf.SetString(tail, style, p) } func (self *ProcWidget) filterProcs(procs []Proc) []Proc { From 9c0253ef3193d8259ba12cc51b3c2b694f309485 Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Tue, 4 Jun 2019 15:08:02 -0700 Subject: [PATCH 11/13] Factor Entry widget out of proc. --- src/termui/entry.go | 132 ++++++++++++++++++++++++++++++++++++++++++++ src/widgets/proc.go | 103 ++++++---------------------------- 2 files changed, 150 insertions(+), 85 deletions(-) create mode 100644 src/termui/entry.go diff --git a/src/termui/entry.go b/src/termui/entry.go new file mode 100644 index 0000000..e0930f7 --- /dev/null +++ b/src/termui/entry.go @@ -0,0 +1,132 @@ +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 "", "": + self.Value = "" + self.editing = false + self.update() + case "": + self.editing = false + case "": + if self.Value != "" { + r := []rune(self.Value) + self.Value = string(r[:len(r)-1]) + self.update() + } + case "": + self.Value += " " + self.update() + default: + return false + } + return true +} + +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 && width <= w; i-- { + cw := rw.RuneWidth(r[i]) + width += cw + if width > w { + break + } + } + return prefix + string(r[i+1:len(r)]) +} + +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/src/widgets/proc.go b/src/widgets/proc.go index a598498..106d970 100644 --- a/src/widgets/proc.go +++ b/src/widgets/proc.go @@ -2,28 +2,23 @@ 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" - rw "github.com/mattn/go-runewidth" ) const ( UP_ARROW = "▲" DOWN_ARROW = "▼" - ELLIPSIS = "…" - CURSOR = " " ) type ProcSortMethod string @@ -44,11 +39,11 @@ type Proc struct { type ProcWidget struct { *ui.Table + entry *ui.Entry cpuCount int updateInterval time.Duration sortMethod ProcSortMethod filter string - editingFilter bool groupedProcs []Proc ungroupedProcs []Proc showGroupedProcs bool @@ -66,7 +61,15 @@ func NewProcWidget() *ProcWidget { sortMethod: ProcSortCpu, showGroupedProcs: true, filter: "", - editingFilter: false, + } + self.entry = &ui.Entry{ + Style: self.TitleStyle, + Label: " Filter: ", + Value: "foobar", + UpdateCallback: func(val string) { + self.filter = val + self.update() + }, } self.Title = " Processes " self.ShowCursor = true @@ -98,91 +101,21 @@ func NewProcWidget() *ProcWidget { } func (self *ProcWidget) SetEditingFilter(editing bool) { - self.editingFilter = editing - if !editing { - self.update() - } + self.entry.SetEditing(editing) } -// handleEditFilterEvents handles events while editing the proc filter. -// Returns true if the event was handled. func (self *ProcWidget) HandleEvent(e tui.Event) bool { - if !self.editingFilter { - return false - } - if utf8.RuneCountInString(e.ID) == 1 { - self.filter += e.ID - self.update() - return true - } - switch e.ID { - case "", "": - self.filter = "" - self.update() - self.SetEditingFilter(false) - case "": - self.SetEditingFilter(false) - case "": - if self.filter != "" { - r := []rune(self.filter) - self.filter = string(r[:len(r)-1]) - self.update() - } - case "": - self.filter += " " - self.update() - default: - return false - } - return true + 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) - if self.filter != "" || self.editingFilter { - self.drawFilter(buf) - } -} - -func (self *ProcWidget) drawFilter(buf *tui.Buffer) { - padding := 2 - - style := self.TitleStyle - label := " Filter: " - if self.editingFilter { - label += "[" - style = tui.NewStyle(style.Fg, style.Bg, tui.ModifierBold) - } - cursorStyle := tui.NewStyle(style.Bg, style.Fg, tui.ModifierClear) - - p := image.Pt(self.Min.X+padding, self.Max.Y-1) - buf.SetString(label, style, p) - p.X += rw.StringWidth(label) - - tail := " " - if self.editingFilter { - tail = "] " - } - - maxLen := self.Max.X - p.X - padding - rw.StringWidth(tail) - if self.editingFilter { - maxLen -= 1 // for cursor - } - - filter := utils.TruncateFront(self.filter, maxLen, ELLIPSIS) - buf.SetString(filter, self.TitleStyle, p) - p.X += rw.StringWidth(filter) - - if self.editingFilter { - buf.SetString(CURSOR, cursorStyle, p) - p.X += rw.StringWidth(CURSOR) - - if remaining := maxLen - rw.StringWidth(filter); remaining > 0 { - buf.SetString(strings.Repeat(" ", remaining), self.TitleStyle, p) - p.X += remaining - } - } - buf.SetString(tail, style, p) + self.entry.Draw(buf) } func (self *ProcWidget) filterProcs(procs []Proc) []Proc { From 129f6c238a35ae406cb2db9dbf8239fde76923f0 Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Tue, 4 Jun 2019 15:11:58 -0700 Subject: [PATCH 12/13] Remove default filter (oops). --- src/widgets/proc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/proc.go b/src/widgets/proc.go index 106d970..748fb64 100644 --- a/src/widgets/proc.go +++ b/src/widgets/proc.go @@ -65,7 +65,7 @@ func NewProcWidget() *ProcWidget { self.entry = &ui.Entry{ Style: self.TitleStyle, Label: " Filter: ", - Value: "foobar", + Value: "", UpdateCallback: func(val string) { self.filter = val self.update() From 246ebfbff2c87f66218c42ea9490214006f2efc8 Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Thu, 6 Jun 2019 11:08:05 -0700 Subject: [PATCH 13/13] Remove unused duplicate definition. --- src/termui/entry.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/termui/entry.go b/src/termui/entry.go index e0930f7..1bdcb39 100644 --- a/src/termui/entry.go +++ b/src/termui/entry.go @@ -71,25 +71,6 @@ func (self *Entry) HandleEvent(e Event) bool { return true } -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 && width <= w; i-- { - cw := rw.RuneWidth(r[i]) - width += cw - if width > w { - break - } - } - return prefix + string(r[i+1:len(r)]) -} - func (self *Entry) Draw(buf *Buffer) { if self.Value == "" && !self.editing && !self.ShowWhenEmpty { return