Initial commit

This commit is contained in:
Caleb Bassi 2018-02-18 23:25:02 -08:00
commit 40775db60b
27 changed files with 2011 additions and 0 deletions

98
README.md Normal file
View 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
View 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
View File

98
termui/block.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}