Initial commit
This commit is contained in:
commit
40775db60b
98
README.md
Normal file
98
README.md
Normal file
|
@ -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:
|
||||||
|
* `<up>`/`<down>` 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
|
||||||
|
* `<tab>`: toggle process grouping
|
||||||
|
* `dd`: kill the selected process or process group
|
||||||
|
* `<left>`/`<right>` 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
|
164
gotop.go
Normal file
164
gotop.go
Normal file
|
@ -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("<escape>", 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()
|
||||||
|
}
|
0
packages/PKGBUILD
Normal file
0
packages/PKGBUILD
Normal file
98
termui/block.go
Normal file
98
termui/block.go
Normal file
|
@ -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
|
||||||
|
}
|
18
termui/block_common.go
Normal file
18
termui/block_common.go
Normal file
|
@ -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 = '»'
|
||||||
|
)
|
18
termui/block_windows.go
Normal file
18
termui/block_windows.go
Normal file
|
@ -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 = '>'
|
||||||
|
)
|
122
termui/buffer.go
Normal file
122
termui/buffer.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
termui/colors.go
Normal file
28
termui/colors.go
Normal file
|
@ -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 ----------------------------- */
|
177
termui/events.go
Normal file
177
termui/events.go
Normal file
|
@ -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 = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
|
||||||
|
} else if e.Key > 0xFFFF-25 {
|
||||||
|
ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
|
||||||
|
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-", "<space>"},
|
||||||
|
tb.KeyBackspace: {"", "<backspace>"},
|
||||||
|
tb.KeyTab: {"", "<tab>"},
|
||||||
|
tb.KeyEnter: {"", "<enter>"},
|
||||||
|
tb.KeyEsc: {"", "<escape>"},
|
||||||
|
tb.KeyCtrlBackslash: {"C-", "\\"},
|
||||||
|
tb.KeyCtrlSlash: {"C-", "/"},
|
||||||
|
tb.KeySpace: {"", "<space>"},
|
||||||
|
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
|
||||||
|
}
|
54
termui/gauge.go
Normal file
54
termui/gauge.go
Normal file
|
@ -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
|
||||||
|
}
|
60
termui/grid.go
Normal file
60
termui/grid.go
Normal file
|
@ -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
|
||||||
|
}
|
28
termui/init.go
Normal file
28
termui/init.go
Normal file
|
@ -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()
|
||||||
|
}
|
100
termui/linegraph.go
Normal file
100
termui/linegraph.go
Normal file
|
@ -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
|
||||||
|
}
|
40
termui/list.go
Normal file
40
termui/list.go
Normal file
|
@ -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
|
||||||
|
}
|
47
termui/render.go
Normal file
47
termui/render.go
Normal file
|
@ -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))
|
||||||
|
}
|
114
termui/sparkline.go
Normal file
114
termui/sparkline.go
Normal file
|
@ -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
|
||||||
|
}
|
182
termui/table.go
Normal file
182
termui/table.go
Normal file
|
@ -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()
|
||||||
|
}
|
48
termui/theme.go
Normal file
48
termui/theme.go
Normal file
|
@ -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)
|
||||||
|
}
|
22
termui/utils.go
Normal file
22
termui/utils.go
Normal file
|
@ -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)
|
||||||
|
}
|
17
utils/utils.go
Normal file
17
utils/utils.go
Normal file
|
@ -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)))
|
||||||
|
}
|
44
widgets/cpu.go
Normal file
44
widgets/cpu.go
Normal file
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
37
widgets/disk.go
Normal file
37
widgets/disk.go
Normal file
|
@ -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)))
|
||||||
|
}
|
52
widgets/help.go
Normal file
52
widgets/help.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
ui "github.com/cjbassi/gotop/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
const KEYBINDS = `
|
||||||
|
Quit: 'q' or 'Ctrl-c'
|
||||||
|
|
||||||
|
Navigation:
|
||||||
|
- '<up>'/'<down>' 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
|
||||||
|
|
||||||
|
'<tab>': toggle process grouping
|
||||||
|
'dd': kill the selected process or process group
|
||||||
|
'<left>'/'<right>' 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
|
||||||
|
}
|
39
widgets/mem.go
Normal file
39
widgets/mem.go
Normal file
|
@ -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)
|
||||||
|
}
|
56
widgets/net.go
Normal file
56
widgets/net.go
Normal file
|
@ -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
|
||||||
|
}
|
302
widgets/proc.go
Normal file
302
widgets/proc.go
Normal file
|
@ -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("<up>", "<down>", func(e ui.Event) {
|
||||||
|
switch e.Key {
|
||||||
|
case "<up>":
|
||||||
|
p.Up()
|
||||||
|
case "<down>":
|
||||||
|
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("<tab>", 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
|
||||||
|
}
|
46
widgets/temp.go
Normal file
46
widgets/temp.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user