commit 40775db60b90cd290a206108cc4d22b236be9ba5 Author: Caleb Bassi Date: Sun Feb 18 23:25:02 2018 -0800 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..b32f870 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# gotop + +![image](demo.gif) + +Another TUI graphical activity monitor, inspired by [vtop](https://github.com/MrRio/vtop) and [gtop](https://github.com/aksakalli/gtop), this time written in [Go](https://golang.org/)! +Built with [gopsutil](https://github.com/shirou/gopsutil), [drawille-go](https://github.com/exrook/drawille-go), and a heavily modified version of [termui](https://github.com/gizak/termui) which uses [termbox-go](https://github.com/nsf/termbox-go). + + +## Installation + +Go code compiles to a single executable so you just need to somehow get that into your $PATH. + +Either manually download the latest release for your OS from the releases tab and move it into `/bin`, or run this command to do it for you: + + sudo ... + +If you install this way, starting gotop with the `-u` flag or pressing `u` in gotop will check to see if there is a newer version available and automatically update if so. + + +### Arch Linux + +Alternatively, if you're on Arch Linux, you can install the `gotop` package from the AUR. + + +## Keybinds + +* Quit: `q` or `Ctrl-c` +* Navigation: + * ``/`` and `j`/`k`: up and down + * `C-d` and `C-u`: up and down half a page + * `C-f` and `C-b`: up and down a full page + * `gg` and `G`: jump to top and bottom +* Process Sorting: + * `c`: CPU + * `m`: Mem + * `p`: PID +* ``: toggle process grouping +* `dd`: kill the selected process or process group +* ``/`` and `h`/`l`: zoom in and out of graphs +* `?`: toggles keybind help menu +* `u`: update gotop + + +## Mouse Control + +* mouse wheel to scroll processes +* click to select process + + +## Colorschemes + +A different Colorscheme can be set with the `-c` startup flag followed its name, all of which can be found in the `colorschemes` folder. +Feel free to add a new one. You can use 256 colors for them, including bold, underline, and reverse. More info [here](https://godoc.org/github.com/nsf/termbox-go#Attribute) and [here](https://godoc.org/github.com/nsf/termbox-go#OutputMode) under Output256. + + +## TODO + +* Network Usage + - increase height of sparkline depending on widget size + +* Process List + - memory total goes above 100% + - extract out column position logic into a function + +* Disk Usage + - color percentage + - change bar color to white + +* general + - option to set polling interval for CPU and mem + - more themes + - zooming in and out of graphs + - updating + - option to only show processes, CPU, and mem + - gif of gotop + - more packages + - add license + +* cleaning up code + - termui Blocks should ignore writing to the outside area + - Ignore writes to outside of inner area, or give error? + - termui Blocks should be indexed at 0, and maybe change X and Y variables too + - remove gotop unique logic from termui widgets + - try to get drawille fork merged upstream + - more documentation + - Draw borders and label after other stuff + - Only merge stuff in the range + - Merge should include offset + - Remove merge from grid buffer function, just render + - Remove merge altogether + + +## License + +AGPLv3 + +Windows is untested +Support colors for more CPU cores diff --git a/gotop.go b/gotop.go new file mode 100644 index 0000000..ce2c04a --- /dev/null +++ b/gotop.go @@ -0,0 +1,164 @@ +package main + +import ( + // "fmt" + "os" + "os/exec" + "os/signal" + "syscall" + "time" + + ui "github.com/cjbassi/gotop/termui" + w "github.com/cjbassi/gotop/widgets" + "github.com/docopt/docopt-go" +) + +const VERSION = "1.0.0" + +var ( + resized = make(chan bool, 1) + + helpToggled = make(chan bool, 1) + helpStatus = false + + procLoaded = make(chan bool, 1) + keyPressed = make(chan bool, 1) + + cpu = w.NewCPU() + mem = w.NewMem() + proc = w.NewProc(procLoaded, keyPressed) + net = w.NewNet() + disk = w.NewDisk() + temp = w.NewTemp() + + help = w.NewHelpMenu() +) + +// Sets up docopt which is a command line argument parser +func docoptInit() { + usage := ` +Usage: gotop [options] + +Options: + -c, --color Set a colorscheme. + -h, --help Show this screen. + -u, --upgrade Updates gotop if needed. + -v, --version Show version. + +Colorschemes: + default +` + + args, _ := docopt.ParseArgs(usage, os.Args[1:], VERSION) + if val, _ := args["--upgrade"]; val.(bool) { + updateGotop() + os.Exit(0) + } + if val, _ := args["--color"]; val.(bool) { + } +} + +func setupGrid() { + ui.Body.Cols = 12 + ui.Body.Rows = 12 + + ui.Body.Set(0, 0, 12, 4, cpu) + + ui.Body.Set(0, 4, 4, 6, disk) + ui.Body.Set(0, 6, 4, 8, temp) + ui.Body.Set(4, 4, 12, 8, mem) + + ui.Body.Set(0, 8, 6, 12, net) + ui.Body.Set(6, 8, 12, 12, proc) +} + +func keyBinds() { + // quits + ui.On("q", "C-c", func(e ui.Event) { + ui.StopLoop() + }) + + // toggles help menu + ui.On("?", func(e ui.Event) { + helpToggled <- true + helpStatus = !helpStatus + }) + // hides help menu + ui.On("", func(e ui.Event) { + if helpStatus { + helpToggled <- true + helpStatus = false + } + }) +} + +func updateGotop() { + cmd := exec.Command("sleep", "1") + cmd.Run() + return +} + +func main() { + docoptInit() + + keyBinds() + + <-procLoaded + + err := ui.Init() + if err != nil { + panic(err) + } + defer ui.Close() + + setupGrid() + + ui.On("resize", func(e ui.Event) { + ui.Body.Width, ui.Body.Height = e.Width, e.Height + ui.Body.Resize() + resized <- true + }) + + // All rendering done here + go func() { + ui.Render(ui.Body) + drawTick := time.NewTicker(time.Second) + for { + select { + case <-helpToggled: + if helpStatus { + ui.Clear() + ui.Render(help) + } else { + ui.Render(ui.Body) + } + case <-resized: + if !helpStatus { + ui.Clear() + ui.Render(ui.Body) + } else if helpStatus { + ui.Clear() + ui.Render(help) + } + case <-keyPressed: + if !helpStatus { + ui.Render(proc) + } + case <-drawTick.C: + if !helpStatus { + ui.Render(ui.Body) + } + } + } + }() + + // handles kill signal + c := make(chan os.Signal, 2) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + ui.StopLoop() + }() + + ui.Loop() +} diff --git a/packages/PKGBUILD b/packages/PKGBUILD new file mode 100644 index 0000000..e69de29 diff --git a/termui/block.go b/termui/block.go new file mode 100644 index 0000000..2367ecf --- /dev/null +++ b/termui/block.go @@ -0,0 +1,98 @@ +package termui + +import "image" + +// Block is a base struct for all other upper level widgets, +// consider it as css: display:block. +// Normally you do not need to create it manually. +type Block struct { + Grid image.Rectangle + X int + Y int + XOffset int + YOffset int + Label string + BorderFg Attribute + BorderBg Attribute + LabelFg Attribute + LabelBg Attribute + Bg Attribute + Fg Attribute +} + +// NewBlock returns a *Block which inherits styles from current theme. +func NewBlock() *Block { + return &Block{ + Fg: Theme.Fg, + Bg: Theme.Bg, + BorderFg: Theme.BorderFg, + BorderBg: Theme.BorderBg, + LabelFg: Theme.LabelFg, + LabelBg: Theme.LabelBg, + } +} + +// Buffer draws a box border. +func (b *Block) drawBorder(buf *Buffer) { + x := b.X + 1 + y := b.Y + 1 + + // draw lines + buf.Merge(NewFilledBuffer(0, 0, x, 1, Cell{HORIZONTAL_LINE, b.BorderFg, b.BorderBg})) + buf.Merge(NewFilledBuffer(0, y, x, y+1, Cell{HORIZONTAL_LINE, b.BorderFg, b.BorderBg})) + buf.Merge(NewFilledBuffer(0, 0, 1, y+1, Cell{VERTICAL_LINE, b.BorderFg, b.BorderBg})) + buf.Merge(NewFilledBuffer(x, 0, x+1, y+1, Cell{VERTICAL_LINE, b.BorderFg, b.BorderBg})) + + // draw corners + buf.SetCell(0, 0, Cell{TOP_LEFT, b.BorderFg, b.BorderBg}) + buf.SetCell(x, 0, Cell{TOP_RIGHT, b.BorderFg, b.BorderBg}) + buf.SetCell(0, y, Cell{BOTTOM_LEFT, b.BorderFg, b.BorderBg}) + buf.SetCell(x, y, Cell{BOTTOM_RIGHT, b.BorderFg, b.BorderBg}) +} + +func (b *Block) drawLabel(buf *Buffer) { + r := MaxString(b.Label, (b.X-3)-1) + buf.SetString(3, 0, r, b.LabelFg, b.LabelBg) + if b.Label == "" { + return + } + c := Cell{' ', b.Fg, b.Bg} + buf.SetCell(2, 0, c) + if len(b.Label)+3 < b.X { + buf.SetCell(len(b.Label)+3, 0, c) + } else { + buf.SetCell(b.X-1, 0, c) + } +} + +// Resize computes Height, Width, XOffset, and YOffset +func (b *Block) Resize(termWidth, termHeight, termCols, termRows int) { + b.X = int((float64(b.Grid.Dx())/float64(termCols))*float64(termWidth)) - 2 + b.Y = int((float64(b.Grid.Dy())/float64(termRows))*float64(termHeight)) - 2 + b.XOffset = int(((float64(b.Grid.Min.X) / float64(termCols)) * float64(termWidth))) + b.YOffset = int(((float64(b.Grid.Min.Y) / float64(termRows)) * float64(termHeight))) +} + +func (b *Block) SetGrid(c0, r0, c1, r1 int) { + b.Grid = image.Rect(c0, r0, c1, r1) +} + +func (b *Block) GetXOffset() int { + return b.XOffset +} + +func (b *Block) GetYOffset() int { + return b.YOffset +} + +// Buffer implements Bufferer interface and draws background and border +func (b *Block) Buffer() *Buffer { + buf := NewBuffer() + buf.SetAreaXY(b.X+2, b.Y+2) + buf.Fill(Cell{' ', ColorDefault, b.Bg}) + + b.drawBorder(buf) + b.drawLabel(buf) + + return buf +} diff --git a/termui/block_common.go b/termui/block_common.go new file mode 100644 index 0000000..aeef1c8 --- /dev/null +++ b/termui/block_common.go @@ -0,0 +1,18 @@ +// +build !windows + +package termui + +const ( + TOP_RIGHT = '┐' + VERTICAL_LINE = '│' + HORIZONTAL_LINE = '─' + TOP_LEFT = '┌' + BOTTOM_RIGHT = '┘' + BOTTOM_LEFT = '└' + VERTICAL_LEFT = '┤' + VERTICAL_RIGHT = '├' + HORIZONTAL_DOWN = '┬' + HORIZONTAL_UP = '┴' + QUOTA_LEFT = '«' + QUOTA_RIGHT = '»' +) diff --git a/termui/block_windows.go b/termui/block_windows.go new file mode 100644 index 0000000..af0307f --- /dev/null +++ b/termui/block_windows.go @@ -0,0 +1,18 @@ +// +build windows + +package termui + +const ( + TOP_RIGHT = '+' + VERTICAL_LINE = '|' + HORIZONTAL_LINE = '-' + TOP_LEFT = '+' + BOTTOM_RIGHT = '+' + BOTTOM_LEFT = '+' + VERTICAL_LEFT = '+' + VERTICAL_RIGHT = '+' + HORIZONTAL_DOWN = '+' + HORIZONTAL_UP = '+' + QUOTA_LEFT = '<' + QUOTA_RIGHT = '>' +) diff --git a/termui/buffer.go b/termui/buffer.go new file mode 100644 index 0000000..c17c989 --- /dev/null +++ b/termui/buffer.go @@ -0,0 +1,122 @@ +package termui + +import "image" + +// Cell is a rune with assigned Fg and Bg +type Cell struct { + Ch rune + Fg Attribute + Bg Attribute +} + +// Buffer is a renderable rectangle cell data container. +type Buffer struct { + Area image.Rectangle // selected drawing area + CellMap map[image.Point]Cell +} + +func NewCell(ch rune, Fg, Bg Attribute) Cell { + return Cell{ch, Fg, Bg} +} + +// NewBuffer returns a new Buffer +func NewBuffer() *Buffer { + return &Buffer{ + CellMap: make(map[image.Point]Cell), + Area: image.Rectangle{}} +} + +// NewFilledBuffer returns a new Buffer filled with ch, fb and bg. +func NewFilledBuffer(x0, y0, x1, y1 int, c Cell) *Buffer { + buf := NewBuffer() + buf.Area.Min = image.Pt(x0, y0) + buf.Area.Max = image.Pt(x1, y1) + + for x := buf.Area.Min.X; x < buf.Area.Max.X; x++ { + for y := buf.Area.Min.Y; y < buf.Area.Max.Y; y++ { + buf.SetCell(x, y, c) + } + } + return buf +} + +// Set assigns a char to (x,y) +func (b *Buffer) SetCell(x, y int, c Cell) { + b.CellMap[image.Pt(x, y)] = c +} + +func (b *Buffer) SetString(x, y int, s string, fg, bg Attribute) { + for i, char := range s { + b.SetCell(x+i, y, Cell{char, fg, bg}) + } +} + +// At returns the cell at (x,y). +func (b *Buffer) At(x, y int) Cell { + return b.CellMap[image.Pt(x, y)] +} + +// Bounds returns the domain for which At can return non-zero color. +func (b *Buffer) Bounds() image.Rectangle { + x0, y0, x1, y1 := 0, 0, 0, 0 + for p := range b.CellMap { + if p.X > x1 { + x1 = p.X + } + if p.X < x0 { + x0 = p.X + } + if p.Y > y1 { + y1 = p.Y + } + if p.Y < y0 { + y0 = p.Y + } + } + return image.Rect(x0, y0, x1+1, y1+1) +} + +// SetArea assigns a new rect area to Buffer b. +func (b *Buffer) SetArea(r image.Rectangle) { + b.Area.Max = r.Max + b.Area.Min = r.Min +} + +func (b *Buffer) SetAreaXY(x, y int) { + b.Area.Min.Y = 0 + b.Area.Min.X = 0 + b.Area.Max.Y = y + b.Area.Max.X = x +} + +// Sync sets drawing area to the buffer's bound +func (b *Buffer) Sync() { + b.SetArea(b.Bounds()) +} + +// Merge merges bs Buffers onto b +func (b *Buffer) Merge(bs ...*Buffer) { + for _, buf := range bs { + for p, c := range buf.CellMap { + b.SetCell(p.X, p.Y, c) + } + b.SetArea(b.Area.Union(buf.Area)) + } +} + +func (b *Buffer) MergeWithOffset(buf *Buffer, xOffset, yOffset int) { + for p, c := range buf.CellMap { + b.SetCell(p.X+xOffset, p.Y+yOffset, c) + } + rect := image.Rect(xOffset, yOffset, buf.Area.Max.X+xOffset, buf.Area.Max.Y+yOffset) + b.SetArea(b.Area.Union(rect)) +} + +// Fill fills the Buffer b with ch,fg and bg. +func (b *Buffer) Fill(c Cell) { + for x := b.Area.Min.X; x < b.Area.Max.X; x++ { + for y := b.Area.Min.Y; y < b.Area.Max.Y; y++ { + b.SetCell(x, y, c) + } + } +} diff --git a/termui/colors.go b/termui/colors.go new file mode 100644 index 0000000..e9a7dda --- /dev/null +++ b/termui/colors.go @@ -0,0 +1,28 @@ +package termui + +/* ---------------Port from termbox-go --------------------- */ + +// Attribute is printable cell's color and style. +type Attribute uint16 + +const ( + ColorDefault Attribute = iota + ColorBlack + ColorRed + ColorGreen + ColorYellow + ColorBlue + ColorMagenta + ColorCyan + ColorWhite +) + +const NumberofColors = 8 + +const ( + AttrBold Attribute = 1 << (iota + 9) + AttrUnderline + AttrReverse +) + +/* ----------------------- End ----------------------------- */ diff --git a/termui/events.go b/termui/events.go new file mode 100644 index 0000000..836740b --- /dev/null +++ b/termui/events.go @@ -0,0 +1,177 @@ +package termui + +import ( + "strconv" + + tb "github.com/nsf/termbox-go" +) + +var eventStream = EventStream{ + make(map[string]func(Event)), + "", + make(chan bool, 1), + make(chan tb.Event), +} + +type EventStream struct { + eventHandlers map[string]func(Event) + prevKey string + stopLoop chan bool + eventQueue chan tb.Event +} + +type Event struct { + Key string + Width int + Height int + MouseX int + MouseY int +} + +// handleEvent calls the approriate callback function if there is one. +func handleEvent(e tb.Event) { + if e.Type == tb.EventError { + panic(e.Err) + } + + ne := convertTermboxEvent(e) + + if val, ok := eventStream.eventHandlers[ne.Key]; ok { + val(ne) + eventStream.prevKey = "" + } else { // check if the last 2 keys form a key combo with a handler + // if this is a keyboard event and the previous event was unhandled + if e.Type == tb.EventKey && eventStream.prevKey != "" { + combo := eventStream.prevKey + ne.Key + if val, ok := eventStream.eventHandlers[combo]; ok { + ne.Key = combo + val(ne) + eventStream.prevKey = "" + } else { + eventStream.prevKey = ne.Key + } + } else { + eventStream.prevKey = ne.Key + } + } +} + +// Loop gets events from termbox and passes them off to handleEvent. +// Stops when StopLoop is called. +func Loop() { + go func() { + for { + eventStream.eventQueue <- tb.PollEvent() + } + }() + + for { + select { + case <-eventStream.stopLoop: + return + case e := <-eventStream.eventQueue: + handleEvent(e) + } + } +} + +// StopLoop stops the events Loop +func StopLoop() { + eventStream.stopLoop <- true +} + +// On assigns event names to their handlers. Takes a string, strings, or a slice of strings, and a function. +func On(things ...interface{}) { + function := things[len(things)-1].(func(Event)) + for _, thing := range things { + if value, ok := thing.(string); ok { + eventStream.eventHandlers[value] = function + } + if value, ok := thing.([]string); ok { + for _, name := range value { + eventStream.eventHandlers[name] = function + } + } + } +} + +// convertTermboxKeyValue converts a termbox keyboard event to a more friendly string format. +// Combines modifiers into the string instead of having them as additional fields in an event. +func convertTermboxKeyValue(e tb.Event) string { + k := string(e.Ch) + pre := "" + mod := "" + + if e.Mod == tb.ModAlt { + mod = "M-" + } + if e.Ch == 0 { + if e.Key > 0xFFFF-12 { + k = "" + } else if e.Key > 0xFFFF-25 { + ks := []string{"", "", "", "", "", "", "", "", "", ""} + k = ks[0xFFFF-int(e.Key)-12] + } + + if e.Key <= 0x7F { + pre = "C-" + k = string('a' - 1 + int(e.Key)) + kmap := map[tb.Key][2]string{ + tb.KeyCtrlSpace: {"C-", ""}, + tb.KeyBackspace: {"", ""}, + tb.KeyTab: {"", ""}, + tb.KeyEnter: {"", ""}, + tb.KeyEsc: {"", ""}, + tb.KeyCtrlBackslash: {"C-", "\\"}, + tb.KeyCtrlSlash: {"C-", "/"}, + tb.KeySpace: {"", ""}, + tb.KeyCtrl8: {"C-", "8"}, + } + if sk, ok := kmap[e.Key]; ok { + pre = sk[0] + k = sk[1] + } + } + } + + return pre + mod + k +} + +// convertTermboxMouseValue turns termbox mouse events into strings +func convertTermboxMouseValue(e tb.Event) string { + switch e.Key { + case tb.MouseLeft: + return "MouseLeft" + case tb.MouseMiddle: + return "MouseMiddle" + case tb.MouseRight: + return "MouseRight" + case tb.MouseWheelUp: + return "MouseWheelUp" + case tb.MouseWheelDown: + return "MouseWheelDown" + case tb.MouseRelease: + return "MouseRelease" + } + return "" +} + +// convertTermboxEvent turns a termbox event into a termui event +func convertTermboxEvent(e tb.Event) Event { + ne := Event{} // new event + + switch e.Type { + case tb.EventKey: + ne.Key = convertTermboxKeyValue(e) + case tb.EventMouse: + ne.Key = convertTermboxMouseValue(e) + ne.MouseX = e.MouseX + ne.MouseY = e.MouseY + case tb.EventResize: + ne.Key = "resize" + ne.Width = e.Width + ne.Height = e.Height + } + + return ne +} diff --git a/termui/gauge.go b/termui/gauge.go new file mode 100644 index 0000000..fbe7b8e --- /dev/null +++ b/termui/gauge.go @@ -0,0 +1,54 @@ +package termui + +import "strconv" + +// Gauge is a progress bar like widget. +type Gauge struct { + *Block + Percent int + BarColor Attribute + PercentColor Attribute + Description string +} + +// NewGauge return a new gauge with current theme. +func NewGauge() *Gauge { + return &Gauge{ + Block: NewBlock(), + PercentColor: Theme.Fg, + BarColor: Theme.Bg, + } +} + +// Buffer implements Bufferer interface. +func (g *Gauge) Buffer() *Buffer { + buf := g.Block.Buffer() + + // plot bar + width := g.Percent * g.X / 100 + for y := 1; y <= g.Y; y++ { + for x := 1; x <= width; x++ { + bg := g.BarColor + if bg == ColorDefault { + bg |= AttrReverse + } + buf.SetCell(x, y, Cell{' ', ColorDefault, bg}) + } + } + + // plot percentage + s := strconv.Itoa(g.Percent) + "%" + g.Description + y := (g.Y + 1) / 2 + s = MaxString(s, g.X) + x := ((g.X - len(s)) + 1) / 2 + + for i, char := range s { + bg := g.Bg + if x+i < width { + bg = AttrReverse + } + buf.SetCell(1+x+i, y, Cell{char, g.PercentColor, bg}) + } + + return buf +} diff --git a/termui/grid.go b/termui/grid.go new file mode 100644 index 0000000..a21c685 --- /dev/null +++ b/termui/grid.go @@ -0,0 +1,60 @@ +package termui + +var Body *Grid + +// GridBufferer introduces a Bufferer that can be manipulated by Grid. +type GridBufferer interface { + Bufferer + Resize(int, int, int, int) + SetGrid(int, int, int, int) +} + +type Grid struct { + Widgets []GridBufferer + Width int + Height int + Cols int + Rows int + BgColor Attribute +} + +func NewGrid() *Grid { + return &Grid{} +} + +func (g *Grid) Set(x0, y0, x1, y1 int, widget GridBufferer) { + if widget == nil { + return + } + if x1 <= x0 || y1 <= y0 { + panic("Invalid widget coordinates") + } + + widget.SetGrid(x0, y0, x1, y1) + widget.Resize(g.Width, g.Height, g.Cols, g.Rows) + + g.Widgets = append(g.Widgets, widget) +} + +func (g *Grid) Resize() { + for _, w := range g.Widgets { + w.Resize(g.Width, g.Height, g.Cols, g.Rows) + } +} + +// Buffer implements Bufferer interface. +func (g *Grid) Buffer() *Buffer { + buf := NewBuffer() + for _, w := range g.Widgets { + buf.MergeWithOffset(w.Buffer(), w.GetXOffset(), w.GetYOffset()) + } + return buf +} + +func (g *Grid) GetXOffset() int { + return 0 +} + +func (g *Grid) GetYOffset() int { + return 0 +} diff --git a/termui/init.go b/termui/init.go new file mode 100644 index 0000000..f9e470d --- /dev/null +++ b/termui/init.go @@ -0,0 +1,28 @@ +package termui + +import tb "github.com/nsf/termbox-go" + +// Init initializes termui library. This function should be called before any others. +// After initialization, the library must be finalized by 'Close' function. +func Init() error { + if err := tb.Init(); err != nil { + return err + } + tb.SetInputMode(tb.InputEsc | tb.InputMouse) + tb.SetOutputMode(tb.Output256) + + Body = NewGrid() + Body.BgColor = Theme.Bg + + // renderLock.Lock() + Body.Width, Body.Height = tb.Size() + // renderLock.Unlock() + + return nil +} + +// Close finalizes termui library, +// should be called after successful initialization when termui's functionality isn't required anymore. +func Close() { + tb.Close() +} diff --git a/termui/linegraph.go b/termui/linegraph.go new file mode 100644 index 0000000..556fdd4 --- /dev/null +++ b/termui/linegraph.go @@ -0,0 +1,100 @@ +package termui + +import ( + "fmt" + "sort" + + drawille "github.com/cjbassi/drawille-go" +) + +// LineGraph implements a graph of data points. +type LineGraph struct { + *Block + Data map[string][]float64 + LineColor map[string]Attribute + + DefaultLineColor Attribute +} + +// NewLineGraph returns a new LineGraph with current theme. +func NewLineGraph() *LineGraph { + return &LineGraph{ + Block: NewBlock(), + Data: make(map[string][]float64), + LineColor: make(map[string]Attribute), + + DefaultLineColor: Theme.LineGraph, + } +} + +// renderPoints plots and interpolates data points. +func (lc *LineGraph) Buffer() *Buffer { + buf := lc.Block.Buffer() + c := drawille.NewCanvas() + colors := make([][]Attribute, lc.X+2) + for i := range colors { + colors[i] = make([]Attribute, lc.Y+2) + } + + // Sort the series so that overlapping data will overlap the same way each time + seriesList := make([]string, len(lc.Data)) + i := 0 + for seriesName := range lc.Data { + seriesList[i] = seriesName + i++ + } + sort.Strings(seriesList) + + for j, seriesName := range seriesList { + seriesData := lc.Data[seriesName] + seriesLineColor, ok := lc.LineColor[seriesName] + if !ok { + seriesLineColor = lc.DefaultLineColor + } + + lastY, lastX := -1, -1 + // assign colors to `colors` and lines/points to the canvas + for i := len(seriesData) - 1; i >= 0; i-- { + x := ((lc.X + 1) * 2) - 1 - (((len(seriesData) - 1) - i) * 5) + y := ((lc.Y + 1) * 4) - 1 - int((float64((lc.Y)*4)-1)*(seriesData[i]/100)) + // stop rendering at the left-most wall + if x < 0 { + break + } + if lastY == -1 { // if this is the first point + c.Set(x, y) + colors[x/2][y/4] = seriesLineColor + } else { + c.DrawLine(lastX, lastY, x, y) + for _, p := range drawille.Line(lastX, lastY, x, y) { + colors[p.X/2][p.Y/4] = seriesLineColor + } + } + lastX, lastY = x, y + } + + // copy drawille and colors to buffer + for y, line := range c.Rows(c.MinX(), c.MinY(), c.MaxX(), c.MaxY()) { + for x, char := range line { + x /= 3 + if x == 0 { + continue + } + if char != 10240 { + buf.SetCell(x, y, Cell{char, colors[x][y], lc.Bg}) + } + } + } + + // Render key + str := fmt.Sprintf("%s %3.0f%%", seriesName, seriesData[len(seriesData)-1]) + for k, char := range str { + if char != ' ' { + buf.SetCell(3+k, j+2, Cell{char, seriesLineColor, lc.Bg}) + } + } + + } + + return buf +} diff --git a/termui/list.go b/termui/list.go new file mode 100644 index 0000000..cb5cd8d --- /dev/null +++ b/termui/list.go @@ -0,0 +1,40 @@ +package termui + +import "fmt" + +// BarChart creates multiple bars in a widget: +type List struct { + *Block + TextColor Attribute + Data []int + DataLabels []string + Threshold int +} + +// NewBarChart returns a new *BarChart with current theme. +func NewList() *List { + return &List{ + Block: NewBlock(), + TextColor: Theme.Fg, + } +} + +// Buffer implements Bufferer interface. +func (bc *List) Buffer() *Buffer { + buf := bc.Block.Buffer() + + for y, text := range bc.DataLabels { + if y+1 > bc.Y { + break + } + bg := ColorGreen + if bc.Data[y] >= bc.Threshold { + bg = ColorRed + } + r := MaxString(text, (bc.X - 4)) + buf.SetString(1, y+1, r, ColorWhite, ColorDefault) + buf.SetString(bc.X-2, y+1, fmt.Sprintf("%dC", bc.Data[y]), bg, ColorDefault) + } + + return buf +} diff --git a/termui/render.go b/termui/render.go new file mode 100644 index 0000000..2d900ab --- /dev/null +++ b/termui/render.go @@ -0,0 +1,47 @@ +package termui + +import ( + "sync" + + tb "github.com/nsf/termbox-go" +) + +var renderJobs chan []Bufferer + +// So that only one render function can flush/write to the screen at a time +// var renderLock sync.Mutex + +// Bufferer should be implemented by all renderable components. Bufferers can render a Buffer. +type Bufferer interface { + Buffer() *Buffer + GetXOffset() int + GetYOffset() int +} + +// Render renders all Bufferer in the given order from left to right, right could overlap on left ones. +func Render(bs ...Bufferer) { + var wg sync.WaitGroup + for _, b := range bs { + wg.Add(1) + go func(b Bufferer) { + defer wg.Done() + buf := b.Buffer() + // set cells in buf + for p, c := range buf.CellMap { + if p.In(buf.Area) { + tb.SetCell(p.X+b.GetXOffset(), p.Y+b.GetYOffset(), c.Ch, tb.Attribute(c.Fg), tb.Attribute(c.Bg)) + } + } + }(b) + } + + // renderLock.Lock() + + wg.Wait() + tb.Flush() + // renderLock.Unlock() +} + +func Clear() { + tb.Clear(tb.ColorDefault, tb.Attribute(Theme.Bg)) +} diff --git a/termui/sparkline.go b/termui/sparkline.go new file mode 100644 index 0000000..2778505 --- /dev/null +++ b/termui/sparkline.go @@ -0,0 +1,114 @@ +package termui + +import ( + "fmt" + "github.com/cjbassi/gotop/utils" +) + +var SPARKS = [8]rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} + +// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers. +type Sparkline struct { + Data []int + Title string + TitleColor Attribute + Total int + LineColor Attribute +} + +// Sparklines is a renderable widget which groups together the given sparklines. +type Sparklines struct { + *Block + Lines []*Sparkline +} + +// Add appends a given Sparkline to s *Sparklines. +func (s *Sparklines) Add(sl Sparkline) { + s.Lines = append(s.Lines, &sl) +} + +// NewSparkline returns a unrenderable single sparkline that intended to be added into Sparklines. +func NewSparkline() *Sparkline { + return &Sparkline{ + TitleColor: Theme.Fg, + LineColor: Theme.SparkLine, + } +} + +// NewSparklines return a new *Sparklines with given Sparkline(s), you can always add a new Sparkline later. +func NewSparklines(ss ...*Sparkline) *Sparklines { + return &Sparklines{ + Block: NewBlock(), + Lines: ss, + } +} + +// Buffer implements Bufferer interface. +func (sl *Sparklines) Buffer() *Buffer { + buf := sl.Block.Buffer() + + lc := len(sl.Lines) // lineCount + + // for each line + for i, line := range sl.Lines { + + // Total and current + y := 2 + (sl.Y/lc)*i + total := "" + title := "" + current := "" + + cur := line.Data[len(line.Data)-1] + curMag := "B" + if cur >= 1000000 { + cur = int(utils.BytesToMB(cur)) + curMag = "MB" + } else if cur >= 1000 { + cur = int(utils.BytesToKB(cur)) + curMag = "kB" + } + + t := line.Total + tMag := "B" + if t >= 1000000000 { + t = int(utils.BytesToGB(t)) + tMag = "GB" + } else if t >= 1000000 { + t = int(utils.BytesToMB(t)) + tMag = "MB" + } + + if i == 0 { + total = fmt.Sprintf(" Total Rx: %3d %s", t, tMag) + current = fmt.Sprintf(" Rx/s: %7d %2s/s", cur, curMag) + } else { + total = fmt.Sprintf(" Total Tx: %3d %s", t, tMag) + current = fmt.Sprintf(" Tx/s: %7d %2s/s", cur, curMag) + } + + total = MaxString(total, sl.X) + title = MaxString(current, sl.X) + buf.SetString(1, y, total, line.TitleColor|AttrBold, sl.Bg) + buf.SetString(1, y+1, title, line.TitleColor|AttrBold, sl.Bg) + + // sparkline + y = (sl.Y / lc) * (i + 1) + // finds max used for relative heights + max := 1 + for i := len(line.Data) - 1; i >= 0 && sl.X-((len(line.Data)-1)-i) >= 1; i-- { + if line.Data[i] > max { + max = line.Data[i] + } + } + // prints sparkline + for x := sl.X; x >= 1; x-- { + char := SPARKS[0] + if (sl.X - x) < len(line.Data) { + char = SPARKS[int((float64(line.Data[(len(line.Data)-1)-(sl.X-x)])/float64(max))*7)] + } + buf.SetCell(x, y, Cell{char, line.LineColor, sl.Bg}) + } + } + + return buf +} diff --git a/termui/table.go b/termui/table.go new file mode 100644 index 0000000..6088986 --- /dev/null +++ b/termui/table.go @@ -0,0 +1,182 @@ +package termui + +import ( + "os/exec" + "strings" +) + +// Table tracks all the attributes of a Table instance +type Table struct { + *Block + Header []string + Rows [][]string + Fg Attribute + Bg Attribute + Cursor Attribute + UniqueCol int + pid string + selected int + topRow int +} + +// NewTable returns a new Table instance +func NewTable() *Table { + return &Table{ + Block: NewBlock(), + Fg: Theme.Fg, + Bg: Theme.Bg, + Cursor: Theme.TableCursor, + selected: 0, + topRow: 0, + UniqueCol: 0, + } +} + +// Buffer ... +func (t *Table) Buffer() *Buffer { + buf := t.Block.Buffer() + + if t.topRow > len(t.Rows)-(t.Y-1) { + t.topRow = len(t.Rows) - (t.Y - 1) + } + + // calculate gap size based on total width + gap := 3 + if t.X < 50 { + gap = 1 + } else if t.X < 75 { + gap = 2 + } + + cw := []int{5, 10, 4, 4} // cellWidth + cp := []int{ // cellPosition + gap, + gap + cw[0] + gap, + t.X - gap - cw[3] - gap - cw[2], + t.X - gap - cw[3], + } + + // total width requires by all 4 columns + contentWidth := gap + cw[0] + gap + cw[1] + gap + cw[2] + gap + cw[3] + gap + render := 4 // number of columns to iterate through + + // removes CPU and MEM if there isn't enough room + if t.X < (contentWidth - gap - cw[3]) { + render = 2 + } else if t.X < contentWidth { + cp[2] = cp[3] + render = 3 + } + + // print header + for i := 0; i < render; i++ { + r := MaxString(t.Header[i], t.X-6) + buf.SetString(cp[i], 1, r, t.Fg|AttrBold, t.Bg) + } + + // prints each row + // for y, row := range t.Rows { + // for y := t.topRow; y <= t.topRow+t.Y; y++ { + for rowNum := t.topRow; rowNum < t.topRow+t.Y-1 && rowNum < len(t.Rows); rowNum++ { + row := t.Rows[rowNum] + y := (rowNum + 2) - t.topRow + + // cursor + bg := t.Bg + if (t.pid == "" && rowNum == t.selected) || (t.pid != "" && t.pid == row[t.UniqueCol]) { + bg = t.Cursor + for i := 0; i < render; i++ { + buf.SetString(1, y, strings.Repeat(" ", t.X), t.Fg, bg) + } + t.pid = row[t.UniqueCol] + t.selected = rowNum + } + + // prints each string + for i := 0; i < render; i++ { + r := MaxString(row[i], t.X-6) + buf.SetString(cp[i], y, r, t.Fg, bg) + } + } + + return buf +} + +//////////////////////////////////////////////////////////////////////////////// + +func (t *Table) calcPos() { + t.pid = "" + + if t.selected < 0 { + t.selected = 0 + } + if t.selected < t.topRow { + t.topRow = t.selected + } + + if t.selected > len(t.Rows)-1 { + t.selected = len(t.Rows) - 1 + } + if t.selected > t.topRow+(t.Y-2) { + t.topRow = t.selected - (t.Y - 2) + } +} + +func (t *Table) Up() { + t.selected -= 1 + t.calcPos() +} + +func (t *Table) Down() { + t.selected += 1 + t.calcPos() +} + +func (t *Table) Top() { + t.selected = 0 + t.calcPos() +} + +func (t *Table) Bottom() { + t.selected = len(t.Rows) - 1 + t.calcPos() +} + +func (t *Table) HalfPageUp() { + t.selected = t.selected - (t.Y-2)/2 + t.calcPos() +} + +func (t *Table) HalfPageDown() { + t.selected = t.selected + (t.Y-2)/2 + t.calcPos() +} + +func (t *Table) PageUp() { + t.selected -= (t.Y - 2) + t.calcPos() +} + +func (t *Table) PageDown() { + t.selected += (t.Y - 2) + t.calcPos() +} + +func (t *Table) Click(x, y int) { + x = x - t.XOffset + y = y - t.YOffset + if (x > 0 && x <= t.X) && (y > 0 && y <= t.Y) { + t.selected = (t.topRow + y) - 2 + t.calcPos() + } +} + +func (t *Table) Kill() { + t.pid = "" + command := "kill" + if t.UniqueCol == 1 { + command = "pkill" + } + cmd := exec.Command(command, t.Rows[t.selected][t.UniqueCol]) + cmd.Start() +} diff --git a/termui/theme.go b/termui/theme.go new file mode 100644 index 0000000..fd1854f --- /dev/null +++ b/termui/theme.go @@ -0,0 +1,48 @@ +package termui + +var Theme = DefaultTheme + +var DefaultTheme = ColorScheme{ + Fg: ColorWhite, + Bg: ColorDefault, + + LabelFg: ColorWhite, + LabelBg: ColorDefault, + BorderFg: ColorCyan, + BorderBg: ColorDefault, + + SparkLine: ColorBlue, + LineGraph: ColorDefault, + TableCursor: ColorBlue, +} + +// A ColorScheme represents the current look-and-feel of the dashboard. +type ColorScheme struct { + Fg Attribute + Bg Attribute + + LabelFg Attribute + LabelBg Attribute + BorderFg Attribute + BorderBg Attribute + + SparkLine Attribute + LineGraph Attribute + TableCursor Attribute +} + +// 0 <= r,g,b <= 5 +func ColorRGB(r, g, b int) Attribute { + within := func(n int) int { + if n < 0 { + return 0 + } + if n > 5 { + return 5 + } + return n + } + + r, b, g = within(r), within(b), within(g) + return Attribute(0x0f + 36*r + 6*g + b) +} diff --git a/termui/utils.go b/termui/utils.go new file mode 100644 index 0000000..c4999be --- /dev/null +++ b/termui/utils.go @@ -0,0 +1,22 @@ +package termui + +import "math" + +const DOTS = '…' + +// MaxString trims a string and adds dots if its length is greater than l +func MaxString(s string, l int) string { + if l <= 0 { + return "" + } + r := []rune(s) + if len(r) > l { + r = r[:l] + r[l-1] = DOTS + } + return string(r) +} + +func Round(f float64) float64 { + return math.Floor(f + .5) +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..684a87b --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,17 @@ +package utils + +import ( + "math" +) + +func BytesToKB(b int) int { + return int((float64(b) / math.Pow10(3))) +} + +func BytesToMB(b int) int { + return int((float64(b) / math.Pow10(6))) +} + +func BytesToGB(b int) int { + return int((float64(b) / math.Pow10(9))) +} diff --git a/widgets/cpu.go b/widgets/cpu.go new file mode 100644 index 0000000..7cdf1a6 --- /dev/null +++ b/widgets/cpu.go @@ -0,0 +1,44 @@ +package widgets + +import ( + "strconv" + "time" + + ui "github.com/cjbassi/gotop/termui" + ps "github.com/shirou/gopsutil/cpu" +) + +type CPU struct { + *ui.LineGraph + count int + interval time.Duration +} + +func NewCPU() *CPU { + count, _ := ps.Counts(false) + c := &CPU{ui.NewLineGraph(), count, time.Second} + c.Label = "CPU Usage" + for i := 0; i < c.count; i++ { + key := "CPU" + strconv.Itoa(i+1) + c.Data[key] = []float64{0} + c.LineColor[key] = ui.Attribute(int(ui.ColorRed) + i) + } + + go c.update() + ticker := time.NewTicker(c.interval) + go func() { + for range ticker.C { + c.update() + } + }() + + return c +} + +func (c *CPU) update() { + percent, _ := ps.Percent(time.Second, true) // takes one second to get the data + for i := 0; i < c.count; i++ { + key := "CPU" + strconv.Itoa(i+1) + c.Data[key] = append(c.Data[key], percent[i]) + } +} diff --git a/widgets/disk.go b/widgets/disk.go new file mode 100644 index 0000000..7b3c2c8 --- /dev/null +++ b/widgets/disk.go @@ -0,0 +1,37 @@ +package widgets + +import ( + "fmt" + "time" + + ui "github.com/cjbassi/gotop/termui" + "github.com/cjbassi/gotop/utils" + ps "github.com/shirou/gopsutil/disk" +) + +type Disk struct { + *ui.Gauge + fs string // which filesystem to get the disk usage of + interval time.Duration +} + +func NewDisk() *Disk { + d := &Disk{ui.NewGauge(), "/", time.Second * 5} + d.Label = "Disk Usage" + + go d.update() + ticker := time.NewTicker(d.interval) + go func() { + for range ticker.C { + d.update() + } + }() + + return d +} + +func (d *Disk) update() { + disk, _ := ps.Usage(d.fs) + d.Percent = int(disk.UsedPercent) + d.Description = fmt.Sprintf(" (%dGB free)", utils.BytesToGB(int(disk.Free))) +} diff --git a/widgets/help.go b/widgets/help.go new file mode 100644 index 0000000..6ebe9b8 --- /dev/null +++ b/widgets/help.go @@ -0,0 +1,52 @@ +package widgets + +import ( + "strings" + + ui "github.com/cjbassi/gotop/termui" +) + +const KEYBINDS = ` +Quit: 'q' or 'Ctrl-c' + +Navigation: + - ''/'' and 'j'/'k': up and down + - 'C-d' and 'C-u': up and down half a page + - 'C-f' and 'C-b': up and down a full page + - 'gg' and 'G': jump to top and bottom + +Process Sorting: + - 'c': CPU + - 'm': Mem + - 'p': PID + +'': toggle process grouping +'dd': kill the selected process or process group +''/'' and 'h'/'l': ... +'u': update gotop +` + +type HelpMenu struct { + ui.Block +} + +func NewHelpMenu() *HelpMenu { + block := *ui.NewBlock() + block.X = 48 + block.Y = 17 + return &HelpMenu{block} +} + +func (hm *HelpMenu) Buffer() *ui.Buffer { + buf := hm.Block.Buffer() + + for y, line := range strings.Split(KEYBINDS, "\n") { + for x, char := range line { + buf.SetCell(x+1, y, ui.NewCell(char, ui.ColorWhite, ui.ColorDefault)) + } + } + + buf.SetAreaXY(100, 100) + + return buf +} diff --git a/widgets/mem.go b/widgets/mem.go new file mode 100644 index 0000000..5778f98 --- /dev/null +++ b/widgets/mem.go @@ -0,0 +1,39 @@ +package widgets + +import ( + "time" + + ui "github.com/cjbassi/gotop/termui" + ps "github.com/shirou/gopsutil/mem" +) + +type Mem struct { + *ui.LineGraph + interval time.Duration +} + +func NewMem() *Mem { + m := &Mem{ui.NewLineGraph(), time.Second} + m.Label = "Memory Usage" + m.Data["Main"] = []float64{0} // Sets initial data to 0 + m.Data["Swap"] = []float64{0} + m.LineColor["Main"] = ui.ColorMagenta + m.LineColor["Swap"] = ui.ColorYellow + + go m.update() + ticker := time.NewTicker(m.interval) + go func() { + for range ticker.C { + m.update() + } + }() + + return m +} + +func (m *Mem) update() { + main, _ := ps.VirtualMemory() + swap, _ := ps.SwapMemory() + m.Data["Main"] = append(m.Data["Main"], main.UsedPercent) + m.Data["Swap"] = append(m.Data["Swap"], swap.UsedPercent) +} diff --git a/widgets/net.go b/widgets/net.go new file mode 100644 index 0000000..b23dea0 --- /dev/null +++ b/widgets/net.go @@ -0,0 +1,56 @@ +package widgets + +import ( + "time" + + ui "github.com/cjbassi/gotop/termui" + ps "github.com/shirou/gopsutil/net" +) + +type Net struct { + *ui.Sparklines + interval time.Duration +} + +func NewNet() *Net { + recv := ui.NewSparkline() + recv.Title = "Receiving" + recv.Data = []int{0} + recv.Total = 0 + + sent := ui.NewSparkline() + sent.Title = "Transfering" + sent.Data = []int{0} + sent.Total = 0 + + spark := ui.NewSparklines(recv, sent) + n := &Net{spark, time.Second} + n.Label = "Network Usage" + + go n.update() + ticker := time.NewTicker(n.interval) + go func() { + for range ticker.C { + n.update() + } + }() + + return n +} + +func (n *Net) update() { + interfaces, _ := ps.IOCounters(false) + recv := int(interfaces[0].BytesRecv) + sent := int(interfaces[0].BytesSent) + + if n.Lines[0].Total != 0 { // if this isn't the first update + curRecv := recv - n.Lines[0].Total + curSent := sent - n.Lines[1].Total + + n.Lines[0].Data = append(n.Lines[0].Data, curRecv) + n.Lines[1].Data = append(n.Lines[1].Data, curSent) + } + + n.Lines[0].Total = recv + n.Lines[1].Total = sent +} diff --git a/widgets/proc.go b/widgets/proc.go new file mode 100644 index 0000000..7a38459 --- /dev/null +++ b/widgets/proc.go @@ -0,0 +1,302 @@ +package widgets + +import ( + "sort" + "strconv" + "time" + + ui "github.com/cjbassi/gotop/termui" + cpu "github.com/shirou/gopsutil/cpu" + ps "github.com/shirou/gopsutil/process" +) + +const ( + DOWN = "▼" + UP = "▲" +) + +// Process represents each process +type Process struct { + PID int32 + Command string + CPU float64 + Mem float32 +} + +// Proc is the widget +type Proc struct { + *ui.Table + cpuCount int + interval time.Duration + sortMethod string + groupedProcs []Process + ungroupedProcs []Process + group bool + KeyPressed chan bool +} + +// Creates a new Proc widget +func NewProc(loaded, keyPressed chan bool) *Proc { + cpuCount, _ := cpu.Counts(false) + p := &Proc{ + Table: ui.NewTable(), + interval: time.Second, + cpuCount: cpuCount, + sortMethod: "c", + group: true, + KeyPressed: keyPressed, + } + p.Label = "Process List" + + p.UniqueCol = 0 + if p.group { + p.UniqueCol = 1 + } + + p.keyBinds() + + go func() { + p.update() + loaded <- true + }() + + ticker := time.NewTicker(p.interval) + go func() { + for range ticker.C { + p.update() + } + }() + + return p +} + +func (p *Proc) update() { + psProcs, _ := ps.Processes() + processes := make([]Process, len(psProcs)) + for i, pr := range psProcs { + pid := pr.Pid + command, _ := pr.Name() + cpu, _ := pr.CPUPercent() + mem, _ := pr.MemoryPercent() + + processes[i] = Process{ + pid, + command, + cpu / float64(p.cpuCount), + mem, + } + } + p.ungroupedProcs = processes + p.groupedProcs = Group(processes) + + p.Sort() +} + +// Sort sorts either the grouped or ungrouped []Process based on the sortMethod +func (p *Proc) Sort() { + p.Header = []string{"Count", "Command", "CPU%", "Mem%"} + + if !p.group { + p.Header[0] = "PID" + } + + processes := &p.ungroupedProcs + if p.group { + processes = &p.groupedProcs + } + + switch p.sortMethod { + case "c": + sort.Sort(sort.Reverse(ProcessByCPU(*processes))) + p.Header[2] += DOWN + case "p": + if p.group { + sort.Sort(sort.Reverse(ProcessByPID(*processes))) + } else { + sort.Sort(ProcessByPID(*processes)) + } + p.Header[0] += DOWN + case "m": + sort.Sort(sort.Reverse(ProcessByMem(*processes))) + p.Header[3] += DOWN + } + + p.Rows = FieldsToStrings(*processes) +} + +func (p *Proc) keyBinds() { + ui.On("MouseLeft", func(e ui.Event) { + p.Click(e.MouseX, e.MouseY) + ui.Render(p) + }) + + ui.On("MouseWheelUp", "MouseWheelDown", func(e ui.Event) { + switch e.Key { + case "MouseWheelDown": + p.Down() + case "MouseWheelUp": + p.Up() + } + p.KeyPressed <- true + }) + + ui.On("", "", func(e ui.Event) { + switch e.Key { + case "": + p.Up() + case "": + p.Down() + } + p.KeyPressed <- true + }) + + viKeys := []string{"j", "k", "gg", "G", "C-d", "C-u", "C-f", "C-b"} + ui.On(viKeys, func(e ui.Event) { + switch e.Key { + case "j": + p.Down() + case "k": + p.Up() + case "gg": + p.Top() + case "G": + p.Bottom() + case "C-d": + p.HalfPageDown() + case "C-u": + p.HalfPageUp() + case "C-f": + p.PageDown() + case "C-b": + p.PageUp() + } + p.KeyPressed <- true + }) + + ui.On("dd", func(e ui.Event) { + p.Kill() + }) + + ui.On("", func(e ui.Event) { + p.group = !p.group + if p.group { + p.UniqueCol = 1 + } else { + p.UniqueCol = 0 + } + p.sortMethod = "c" + p.Sort() + p.Top() + p.KeyPressed <- true + }) + + ui.On("m", "c", "p", func(e ui.Event) { + if p.sortMethod != e.Key { + p.sortMethod = e.Key + p.Top() + p.Sort() + p.KeyPressed <- true + } + }) +} + +// Group groupes a []Process based on command name. +// The first field changes from PID to count. +// CPU and Mem are added together for each Process. +func Group(P []Process) []Process { + groupMap := make(map[string]Process) + for _, p := range P { + val, ok := groupMap[p.Command] + if ok { + newP := Process{ + val.PID + 1, + val.Command, + val.CPU + p.CPU, + val.Mem + p.Mem, + } + groupMap[p.Command] = newP + } else { + newP := Process{ + 1, + p.Command, + p.CPU, + p.Mem, + } + groupMap[p.Command] = newP + } + } + groupList := make([]Process, len(groupMap)) + i := 0 + for _, val := range groupMap { + groupList[i] = val + i++ + } + return groupList +} + +// FieldsToStrings converts a []Process to a [][]string +func FieldsToStrings(P []Process) [][]string { + strings := make([][]string, len(P)) + for i, p := range P { + strings[i] = make([]string, 4) + strings[i][0] = strconv.Itoa(int(p.PID)) + strings[i][1] = p.Command + strings[i][2] = strconv.FormatFloat(p.CPU, 'f', 1, 64) + strings[i][3] = strconv.FormatFloat(float64(p.Mem), 'f', 1, 32) + } + return strings +} + +//////////////////////////////////////////////////////////////////////////////// +// Sorting + +type ProcessByCPU []Process + +// Len implements Sort interface +func (P ProcessByCPU) Len() int { + return len(P) +} + +// Swap implements Sort interface +func (P ProcessByCPU) Swap(i, j int) { + P[i], P[j] = P[j], P[i] +} + +// Less implements Sort interface +func (P ProcessByCPU) Less(i, j int) bool { + return P[i].CPU < P[j].CPU +} + +type ProcessByPID []Process + +// Len implements Sort interface +func (P ProcessByPID) Len() int { + return len(P) +} + +// Swap implements Sort interface +func (P ProcessByPID) Swap(i, j int) { + P[i], P[j] = P[j], P[i] +} + +// Less implements Sort interface +func (P ProcessByPID) Less(i, j int) bool { + return P[i].PID < P[j].PID +} + +type ProcessByMem []Process + +// Len implements Sort interface +func (P ProcessByMem) Len() int { + return len(P) +} + +// Swap implements Sort interface +func (P ProcessByMem) Swap(i, j int) { + P[i], P[j] = P[j], P[i] +} + +// Less implements Sort interface +func (P ProcessByMem) Less(i, j int) bool { + return P[i].Mem < P[j].Mem +} diff --git a/widgets/temp.go b/widgets/temp.go new file mode 100644 index 0000000..e965629 --- /dev/null +++ b/widgets/temp.go @@ -0,0 +1,46 @@ +package widgets + +import ( + "strings" + "time" + + ui "github.com/cjbassi/gotop/termui" + ps "github.com/shirou/gopsutil/host" +) + +type Temp struct { + *ui.List + interval time.Duration +} + +func NewTemp() *Temp { + t := &Temp{ui.NewList(), time.Second * 5} + t.Label = "Temperatures" + t.Threshold = 80 // temp at which color should change to red + + go t.update() + ticker := time.NewTicker(t.interval) + go func() { + for range ticker.C { + t.update() + } + }() + + return t +} + +func (t *Temp) update() { + sensors, _ := ps.SensorsTemperatures() + temps := []int{} + labels := []string{} + for _, temp := range sensors { + // only sensors with input in their name are giving us live temp info + if strings.Contains(temp.SensorKey, "input") { + temps = append(temps, int(temp.Temperature)) + // removes '_input' from the end of the sensor name + labels = append(labels, temp.SensorKey[:strings.Index(temp.SensorKey, "_input")]) + } + } + t.Data = temps + t.DataLabels = labels +}