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