This commit is contained in:
Caleb Bassi 2019-02-28 16:29:52 -08:00
parent 3a804eb666
commit 71e10bb6be
24 changed files with 597 additions and 559 deletions

4
go.sum
View File

@ -10,7 +10,9 @@ github.com/gizak/termui v0.0.0-20190124041613-958a28575d74 h1:gQbT+IgWIflxp7EQax
github.com/gizak/termui v0.0.0-20190124041613-958a28575d74/go.mod h1:hJmkzz29zwvMdxina9wLc5fWN7bZuougH5YR93VrJtA=
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/google/pprof v0.0.0-20190109223431-e84dfd68c163 h1:beB+Da4k9B1zmgag78k3k1Bx4L/fdWr5FwNa0f8RxmY=
github.com/google/pprof v0.0.0-20190109223431-e84dfd68c163/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -36,7 +38,9 @@ github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+D
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 h1:Pn8fQdvx+z1avAi7fdM2kRYWQNxGlavNDSyzrQg2SsU=
golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M=
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

73
main.go
View File

@ -44,19 +44,19 @@ var (
minimalMode = false
averageLoad = false
percpuLoad = false
fahrenheit = false
tempScale = w.Celcius
battery = false
statusbar = false
renderLock sync.RWMutex
cpu *w.CPU
batt *w.Batt
mem *w.Mem
proc *w.Proc
net *w.Net
disk *w.Disk
temp *w.Temp
cpu *w.CpuWidget
batt *w.BatteryWidget
mem *w.MemWidget
proc *w.ProcWidget
net *w.NetWidget
disk *w.DiskWidget
temp *w.TempWidget
help *w.HelpMenu
grid *ui.Grid
bar *w.StatusBar
@ -134,7 +134,10 @@ Colorschemes:
} else {
updateInterval = time.Second / time.Duration(rate)
}
fahrenheit, _ = args["--fahrenheit"].(bool)
fahrenheit, _ := args["--fahrenheit"].(bool)
if fahrenheit {
tempScale = w.Fahrenheit
}
return nil
}
@ -221,8 +224,8 @@ func setDefaultTermuiColors() {
}
func setWidgetColors() {
mem.LineColor["Main"] = ui.Color(colorscheme.MainMem)
mem.LineColor["Swap"] = ui.Color(colorscheme.SwapMem)
mem.LineColors["Main"] = ui.Color(colorscheme.MainMem)
mem.LineColors["Swap"] = ui.Color(colorscheme.SwapMem)
proc.CursorColor = ui.Color(colorscheme.ProcCursor)
@ -238,7 +241,7 @@ func setWidgetColors() {
i = 0
}
c := colorscheme.CPULines[i]
cpu.LineColor[v] = ui.Color(c)
cpu.LineColors[v] = ui.Color(c)
i++
}
@ -256,13 +259,13 @@ func setWidgetColors() {
i = 0
}
c := colorscheme.BattLines[i]
batt.LineColor[v] = ui.Color(c)
batt.LineColors[v] = ui.Color(c)
i++
}
}
temp.TempLow = ui.Color(colorscheme.TempLow)
temp.TempHigh = ui.Color(colorscheme.TempHigh)
temp.TempLowColor = ui.Color(colorscheme.TempLow)
temp.TempHighColor = ui.Color(colorscheme.TempHigh)
net.Lines[0].LineColor = ui.Color(colorscheme.Sparkline)
net.Lines[0].TitleColor = ui.Color(colorscheme.BorderLabel)
@ -272,17 +275,17 @@ func setWidgetColors() {
}
func initWidgets() {
cpu = w.NewCPU(&renderLock, updateInterval, graphHorizontalScale, averageLoad, percpuLoad)
mem = w.NewMem(&renderLock, updateInterval, graphHorizontalScale)
proc = w.NewProc(&renderLock)
cpu = w.NewCpuWidget(&renderLock, updateInterval, graphHorizontalScale, averageLoad, percpuLoad)
mem = w.NewMemWidget(&renderLock, updateInterval, graphHorizontalScale)
proc = w.NewProcWidget(&renderLock)
help = w.NewHelpMenu()
if !minimalMode {
if battery {
batt = w.NewBatt(&renderLock, graphHorizontalScale)
batt = w.NewBatteryWidget(&renderLock, graphHorizontalScale)
}
net = w.NewNet(&renderLock)
disk = w.NewDisk(&renderLock)
temp = w.NewTemp(&renderLock, fahrenheit)
net = w.NewNetWidget(&renderLock)
disk = w.NewDiskWidget(&renderLock)
temp = w.NewTempWidget(&renderLock, tempScale)
}
if statusbar {
bar = w.NewStatusBar()
@ -367,46 +370,46 @@ func eventLoop() {
}
case "<MouseLeft>":
payload := e.Payload.(ui.Mouse)
proc.Click(payload.X, payload.Y)
proc.HandleClick(payload.X, payload.Y)
render(proc)
case "k", "<Up>", "<MouseWheelUp>":
proc.Up()
proc.ScrollUp()
render(proc)
case "j", "<Down>", "<MouseWheelDown>":
proc.Down()
proc.ScrollDown()
render(proc)
case "<Home>":
proc.Top()
proc.ScrollTop()
render(proc)
case "g":
if previousKey == "g" {
proc.Top()
proc.ScrollTop()
render(proc)
}
case "G", "<End>":
proc.Bottom()
proc.ScrollBottom()
render(proc)
case "<C-d>":
proc.HalfPageDown()
proc.ScrollHalfPageDown()
render(proc)
case "<C-u>":
proc.HalfPageUp()
proc.ScrollHalfPageUp()
render(proc)
case "<C-f>":
proc.PageDown()
proc.ScrollPageDown()
render(proc)
case "<C-b>":
proc.PageUp()
proc.ScrollPageUp()
render(proc)
case "d":
if previousKey == "d" {
proc.Kill()
proc.KillProc()
}
case "<Tab>":
proc.Tab()
proc.ToggleShowingGroupedProcs()
render(proc)
case "m", "c", "p":
proc.ChangeSort(e)
proc.ChangeProcSortMethod(w.ProcSortMethod(e.ID))
render(proc)
}

View File

@ -5,6 +5,6 @@ import (
"syscall"
)
func StderrToLogfile(lf *os.File) {
syscall.Dup3(int(lf.Fd()), 2, 0)
func StderrToLogfile(logfile *os.File) {
syscall.Dup3(int(logfile.Fd()), 2, 0)
}

View File

@ -7,6 +7,6 @@ import (
"syscall"
)
func StderrToLogfile(lf *os.File) {
syscall.Dup2(int(lf.Fd()), 2)
func StderrToLogfile(logfile *os.File) {
syscall.Dup2(int(logfile.Fd()), 2)
}

View File

@ -11,21 +11,26 @@ import (
// LineGraph implements a line graph of data points.
type LineGraph struct {
*Block
Data map[string][]float64
LineColor map[string]Color
HorizontalScale int
Labels map[string]string
Data map[string][]float64
Labels map[string]string
HorizontalScale int
LineColors map[string]Color
DefaultLineColor Color
}
func NewLineGraph() *LineGraph {
return &LineGraph{
Block: NewBlock(),
Data: make(map[string][]float64),
LineColor: make(map[string]Color),
Labels: make(map[string]string),
Block: NewBlock(),
Data: make(map[string][]float64),
Labels: make(map[string]string),
HorizontalScale: 5,
LineColors: make(map[string]Color),
}
}
@ -53,7 +58,7 @@ func (self *LineGraph) Draw(buf *Buffer) {
for i := len(seriesList) - 1; i >= 0; i-- {
seriesName := seriesList[i]
seriesData := self.Data[seriesName]
seriesLineColor, ok := self.LineColor[seriesName]
seriesLineColor, ok := self.LineColors[seriesName]
if !ok {
seriesLineColor = self.DefaultLineColor
}
@ -110,7 +115,7 @@ func (self *LineGraph) Draw(buf *Buffer) {
if i+2 > self.Inner.Dy() {
continue
}
seriesLineColor, ok := self.LineColor[seriesName]
seriesLineColor, ok := self.LineColors[seriesName]
if !ok {
seriesLineColor = self.DefaultLineColor
}

View File

@ -16,31 +16,31 @@ type Sparkline struct {
LineColor Color
}
// Sparklines is a renderable widget which groups together the given sparklines.
type Sparklines struct {
// SparklineGroup is a renderable widget which groups together the given sparklines.
type SparklineGroup struct {
*Block
Lines []*Sparkline
}
// Add appends a given Sparkline to the *Sparklines.
func (self *Sparklines) Add(sl Sparkline) {
// Add appends a given Sparkline to the *SparklineGroup.
func (self *SparklineGroup) Add(sl Sparkline) {
self.Lines = append(self.Lines, &sl)
}
// NewSparkline returns an unrenderable single sparkline that intended to be added into a Sparklines.
// NewSparkline returns an unrenderable single sparkline that intended to be added into a SparklineGroup.
func NewSparkline() *Sparkline {
return &Sparkline{}
}
// NewSparklines return a new *Sparklines with given Sparklines, you can always add a new Sparkline later.
func NewSparklines(ss ...*Sparkline) *Sparklines {
return &Sparklines{
// NewSparklineGroup return a new *SparklineGroup with given Sparklines, you can always add a new Sparkline later.
func NewSparklineGroup(ss ...*Sparkline) *SparklineGroup {
return &SparklineGroup{
Block: NewBlock(),
Lines: ss,
}
}
func (self *Sparklines) Draw(buf *Buffer) {
func (self *SparklineGroup) Draw(buf *Buffer) {
self.Block.Draw(buf)
lc := len(self.Lines) // lineCount

View File

@ -14,37 +14,30 @@ type Table struct {
Header []string
Rows [][]string
ColWidths []int
CellXPos []int // column position
ColResizer func() // for widgets that inherit a Table and want to overload the ColResize method
Gap int // gap between columns
PadLeft int
ColWidths []int
ColGap int
PadLeft int
Cursor bool
ShowCursor bool
CursorColor Color
UniqueCol int // the column used to identify the selected item
UniqueCol int // the column used to uniquely identify each table row
SelectedItem string // used to keep the cursor on the correct item if the data changes
SelectedRow int
TopRow int // used to indicate where in the table we are scrolled at
ColResizer func()
}
// NewTable returns a new Table instance
func NewTable() *Table {
self := &Table{
Block: NewBlock(),
// CursorColor: Theme.TableCursor,
return &Table{
Block: NewBlock(),
SelectedRow: 0,
TopRow: 0,
UniqueCol: 0,
ColResizer: func() {},
}
self.ColResizer = self.ColResize
return self
}
// ColResize is the default column resizer, but can be overriden.
// ColResize calculates the width of each column.
func (self *Table) ColResize() {
}
func (self *Table) Draw(buf *Buffer) {
@ -53,12 +46,12 @@ func (self *Table) Draw(buf *Buffer) {
self.ColResizer()
// finds exact column starting position
self.CellXPos = []int{}
colXPos := []int{}
cur := 1 + self.PadLeft
for _, w := range self.ColWidths {
self.CellXPos = append(self.CellXPos, cur)
colXPos = append(colXPos, cur)
cur += w
cur += self.Gap
cur += self.ColGap
}
// prints header
@ -68,13 +61,13 @@ func (self *Table) Draw(buf *Buffer) {
continue
}
// don't render column if it doesn't fit in widget
if width > (self.Inner.Dx()-self.CellXPos[i])+1 {
if width > (self.Inner.Dx()-colXPos[i])+1 {
continue
}
buf.SetString(
h,
NewStyle(Theme.Default.Fg, ColorClear, ModifierBold),
image.Pt(self.Inner.Min.X+self.CellXPos[i]-1, self.Inner.Min.Y),
image.Pt(self.Inner.Min.X+colXPos[i]-1, self.Inner.Min.Y),
)
}
@ -90,7 +83,7 @@ func (self *Table) Draw(buf *Buffer) {
// prints cursor
style := NewStyle(Theme.Default.Fg)
if self.Cursor {
if self.ShowCursor {
if (self.SelectedItem == "" && rowNum == self.SelectedRow) || (self.SelectedItem != "" && self.SelectedItem == row[self.UniqueCol]) {
style.Fg = self.CursorColor
style.Modifier = ModifierReverse
@ -115,24 +108,22 @@ func (self *Table) Draw(buf *Buffer) {
continue
}
// don't render column if width is greater than distance to end of widget
if width > (self.Inner.Dx()-self.CellXPos[i])+1 {
if width > (self.Inner.Dx()-colXPos[i])+1 {
continue
}
r := TrimString(row[i], width)
buf.SetString(
r,
style,
image.Pt(self.Inner.Min.X+self.CellXPos[i]-1, self.Inner.Min.Y+y-1),
image.Pt(self.Inner.Min.X+colXPos[i]-1, self.Inner.Min.Y+y-1),
)
}
}
}
/////////////////////////////////////////////////////////////////////////////////
// Cursor Movement //
/////////////////////////////////////////////////////////////////////////////////
// Scrolling ///////////////////////////////////////////////////////////////////
// calcPos is used to calculate the cursor position and the current view.
// calcPos is used to calculate the cursor position and the current view into the table.
func (self *Table) calcPos() {
self.SelectedItem = ""
@ -151,49 +142,47 @@ func (self *Table) calcPos() {
}
}
func (self *Table) Up() {
func (self *Table) ScrollUp() {
self.SelectedRow--
self.calcPos()
}
func (self *Table) Down() {
func (self *Table) ScrollDown() {
self.SelectedRow++
self.calcPos()
}
func (self *Table) Top() {
func (self *Table) ScrollTop() {
self.SelectedRow = 0
self.calcPos()
}
func (self *Table) Bottom() {
func (self *Table) ScrollBottom() {
self.SelectedRow = len(self.Rows) - 1
self.calcPos()
}
// The number of lines in a page is equal to the height of the widgeself.
func (self *Table) HalfPageUp() {
func (self *Table) ScrollHalfPageUp() {
self.SelectedRow = self.SelectedRow - (self.Inner.Dy()-2)/2
self.calcPos()
}
func (self *Table) HalfPageDown() {
func (self *Table) ScrollHalfPageDown() {
self.SelectedRow = self.SelectedRow + (self.Inner.Dy()-2)/2
self.calcPos()
}
func (self *Table) PageUp() {
func (self *Table) ScrollPageUp() {
self.SelectedRow -= (self.Inner.Dy() - 2)
self.calcPos()
}
func (self *Table) PageDown() {
func (self *Table) ScrollPageDown() {
self.SelectedRow += (self.Inner.Dy() - 2)
self.calcPos()
}
func (self *Table) Click(x, y int) {
func (self *Table) HandleClick(x, y int) {
x = x - self.Min.X
y = y - self.Min.Y
if (x > 0 && x <= self.Inner.Dx()) && (y > 0 && y <= self.Inner.Dy()) {

View File

@ -46,7 +46,7 @@ func ConvertBytes(b uint64) (float64, string) {
}
}
func Max(a, b int) int {
func MaxInt(a, b int) int {
if a > b {
return a
}

View File

@ -13,25 +13,26 @@ import (
ui "github.com/cjbassi/gotop/src/termui"
)
type Batt struct {
type BatteryWidget struct {
*ui.LineGraph
interval time.Duration
updateInterval time.Duration
}
func NewBatt(renderLock *sync.RWMutex, horizontalScale int) *Batt {
self := &Batt{
LineGraph: ui.NewLineGraph(),
interval: time.Minute,
func NewBatteryWidget(renderLock *sync.RWMutex, horizontalScale int) *BatteryWidget {
self := &BatteryWidget{
LineGraph: ui.NewLineGraph(),
updateInterval: time.Minute,
}
self.Title = " Battery Status "
self.HorizontalScale = horizontalScale
// intentional duplicate
// adds 2 datapoints to the graph, otherwise the dot is difficult to see
self.update()
self.update()
go func() {
for range time.NewTicker(self.interval).C {
for range time.NewTicker(self.updateInterval).C {
renderLock.RLock()
self.update()
renderLock.RUnlock()
@ -41,20 +42,20 @@ func NewBatt(renderLock *sync.RWMutex, horizontalScale int) *Batt {
return self
}
func mkId(i int) string {
func makeId(i int) string {
return "Batt" + strconv.Itoa(i)
}
func (self *Batt) update() {
batts, err := battery.GetAll()
func (self *BatteryWidget) update() {
batteries, err := battery.GetAll()
if err != nil {
log.Printf("failed to get battery info from system: %v", err)
log.Printf("failed to get battery info: %v", err)
return
}
for i, b := range batts {
n := mkId(i)
pc := math.Abs(b.Current/b.Full) * 100.0
self.Data[n] = append(self.Data[n], pc)
self.Labels[n] = fmt.Sprintf("%3.0f%% %.0f/%.0f", pc, math.Abs(b.Current), math.Abs(b.Full))
for i, battery := range batteries {
id := makeId(i)
percentFull := math.Abs(battery.Current/battery.Full) * 100.0
self.Data[id] = append(self.Data[id], percentFull)
self.Labels[id] = fmt.Sprintf("%3.0f%% %.0f/%.0f", percentFull, math.Abs(battery.Current), math.Abs(battery.Full))
}
}

View File

@ -6,66 +6,66 @@ import (
"sync"
"time"
psCPU "github.com/shirou/gopsutil/cpu"
psCpu "github.com/shirou/gopsutil/cpu"
ui "github.com/cjbassi/gotop/src/termui"
)
type CPU struct {
type CpuWidget struct {
*ui.LineGraph
Count int // number of cores
Average bool // show average load
PerCPU bool // show per-core load
interval time.Duration
formatString string
renderLock *sync.RWMutex
updateLock sync.Mutex
CpuCount int
ShowAverageLoad bool
ShowPerCpuLoad bool
updateInterval time.Duration
formatString string
renderLock *sync.RWMutex
updateLock sync.Mutex
}
func NewCPU(renderLock *sync.RWMutex, interval time.Duration, horizontalScale int, average bool, percpu bool) *CPU {
count, err := psCPU.Counts(false)
func NewCpuWidget(renderLock *sync.RWMutex, updateInterval time.Duration, horizontalScale int, showAverageLoad bool, showPerCpuLoad bool) *CpuWidget {
cpuCount, err := psCpu.Counts(false)
if err != nil {
log.Printf("failed to get CPU count from gopsutil: %v", err)
}
formatString := "CPU%1d"
if count > 10 {
if cpuCount > 10 {
formatString = "CPU%02d"
}
self := &CPU{
LineGraph: ui.NewLineGraph(),
Count: count,
interval: interval,
Average: average,
PerCPU: percpu,
formatString: formatString,
renderLock: renderLock,
self := &CpuWidget{
LineGraph: ui.NewLineGraph(),
CpuCount: cpuCount,
updateInterval: updateInterval,
ShowAverageLoad: showAverageLoad,
ShowPerCpuLoad: showPerCpuLoad,
formatString: formatString,
renderLock: renderLock,
}
self.Title = " CPU Usage "
self.HorizontalScale = horizontalScale
if !(self.Average || self.PerCPU) {
if self.Count <= 8 {
self.PerCPU = true
if !(self.ShowAverageLoad || self.ShowPerCpuLoad) {
if self.CpuCount <= 8 {
self.ShowPerCpuLoad = true
} else {
self.Average = true
self.ShowAverageLoad = true
}
}
if self.Average {
if self.ShowAverageLoad {
self.Data["AVRG"] = []float64{0}
}
if self.PerCPU {
for i := 0; i < self.Count; i++ {
k := fmt.Sprintf(formatString, i)
self.Data[k] = []float64{0}
if self.ShowPerCpuLoad {
for i := 0; i < int(self.CpuCount); i++ {
key := fmt.Sprintf(formatString, i)
self.Data[key] = []float64{0}
}
}
self.update()
go func() {
for range time.NewTicker(self.interval).C {
for range time.NewTicker(self.updateInterval).C {
self.update()
}
}()
@ -73,12 +73,12 @@ func NewCPU(renderLock *sync.RWMutex, interval time.Duration, horizontalScale in
return self
}
func (self *CPU) update() {
if self.Average {
func (self *CpuWidget) update() {
if self.ShowAverageLoad {
go func() {
percent, err := psCPU.Percent(self.interval, false)
percent, err := psCpu.Percent(self.updateInterval, false)
if err != nil {
log.Printf("failed to get average CPU usage percent from gopsutil: %v. self.interval: %v. percpu: %v", err, self.interval, false)
log.Printf("failed to get average CPU usage percent from gopsutil: %v. self.updateInterval: %v. percpu: %v", err, self.updateInterval, false)
} else {
self.renderLock.RLock()
defer self.renderLock.RUnlock()
@ -90,23 +90,23 @@ func (self *CPU) update() {
}()
}
if self.PerCPU {
if self.ShowPerCpuLoad {
go func() {
percents, err := psCPU.Percent(self.interval, true)
percents, err := psCpu.Percent(self.updateInterval, true)
if err != nil {
log.Printf("failed to get CPU usage percents from gopsutil: %v. self.interval: %v. percpu: %v", err, self.interval, true)
log.Printf("failed to get CPU usage percents from gopsutil: %v. self.updateInterval: %v. percpu: %v", err, self.updateInterval, true)
} else {
if len(percents) != self.Count {
log.Printf("error: number of CPU usage percents from gopsutil doesn't match CPU count. percents: %v. self.Count: %v", percents, self.Count)
if len(percents) != int(self.CpuCount) {
log.Printf("error: number of CPU usage percents from gopsutil doesn't match CPU count. percents: %v. self.Count: %v", percents, self.CpuCount)
} else {
self.renderLock.RLock()
defer self.renderLock.RUnlock()
self.updateLock.Lock()
defer self.updateLock.Unlock()
for i, percent := range percents {
k := fmt.Sprintf(self.formatString, i)
self.Data[k] = append(self.Data[k], percent)
self.Labels[k] = fmt.Sprintf("%3.0f%%", percent)
key := fmt.Sprintf(self.formatString, i)
self.Data[key] = append(self.Data[key], percent)
self.Labels[key] = fmt.Sprintf("%3.0f%%", percent)
}
}
}

View File

@ -15,37 +15,43 @@ import (
)
type Partition struct {
Device string
Mount string
TotalRead uint64
TotalWrite uint64
CurRead string
CurWrite string
UsedPercent int
Free string
Device string
MountPoint string
BytesRead uint64
BytesWritten uint64
BytesReadRecently string
BytesWrittenRecently string
UsedPercent uint32
Free string
}
type Disk struct {
type DiskWidget struct {
*ui.Table
interval time.Duration
Partitions map[string]*Partition
updateInterval time.Duration
Partitions map[string]*Partition
}
func NewDisk(renderLock *sync.RWMutex) *Disk {
self := &Disk{
Table: ui.NewTable(),
interval: time.Second,
Partitions: make(map[string]*Partition),
func NewDiskWidget(renderLock *sync.RWMutex) *DiskWidget {
self := &DiskWidget{
Table: ui.NewTable(),
updateInterval: time.Second,
Partitions: make(map[string]*Partition),
}
self.Title = " Disk Usage "
self.Header = []string{"Disk", "Mount", "Used", "Free", "R/s", "W/s"}
self.Gap = 2
self.ColResizer = self.ColResize
self.ColGap = 2
self.ColResizer = func() {
self.ColWidths = []int{
utils.MaxInt(4, (self.Inner.Dx()-29)/2),
utils.MaxInt(5, (self.Inner.Dx()-29)/2),
4, 5, 5, 5,
}
}
self.update()
go func() {
for range time.NewTicker(self.interval).C {
for range time.NewTicker(self.updateInterval).C {
renderLock.RLock()
self.update()
renderLock.RUnlock()
@ -55,86 +61,87 @@ func NewDisk(renderLock *sync.RWMutex) *Disk {
return self
}
func (self *Disk) update() {
Partitions, err := psDisk.Partitions(false)
func (self *DiskWidget) update() {
partitions, err := psDisk.Partitions(false)
if err != nil {
log.Printf("failed to get disk partitions from gopsutil: %v", err)
return
}
// add partition if it's new
for _, Part := range Partitions {
for _, partition := range partitions {
// don't show loop devices
if strings.HasPrefix(Part.Device, "/dev/loop") {
if strings.HasPrefix(partition.Device, "/dev/loop") {
continue
}
// don't show docker container filesystems
if strings.HasPrefix(Part.Mountpoint, "/var/lib/docker/") {
if strings.HasPrefix(partition.Mountpoint, "/var/lib/docker/") {
continue
}
// check if partition doesn't already exist in our list
if _, ok := self.Partitions[Part.Device]; !ok {
self.Partitions[Part.Device] = &Partition{
Device: Part.Device,
Mount: Part.Mountpoint,
if _, ok := self.Partitions[partition.Device]; !ok {
self.Partitions[partition.Device] = &Partition{
Device: partition.Device,
MountPoint: partition.Mountpoint,
}
}
}
// delete a partition if it no longer exists
todelete := []string{}
for key := range self.Partitions {
toDelete := []string{}
for device := range self.Partitions {
exists := false
for _, Part := range Partitions {
if key == Part.Device {
for _, partition := range partitions {
if device == partition.Device {
exists = true
break
}
}
if !exists {
todelete = append(todelete, key)
toDelete = append(toDelete, device)
}
}
for _, val := range todelete {
delete(self.Partitions, val)
for _, device := range toDelete {
delete(self.Partitions, device)
}
// updates partition info
for _, Part := range self.Partitions {
usage, err := psDisk.Usage(Part.Mount)
for _, partition := range self.Partitions {
usage, err := psDisk.Usage(partition.MountPoint)
if err != nil {
log.Printf("failed to get partition usage statistics from gopsutil: %v. Part: %v", err, Part)
log.Printf("failed to get partition usage statistics from gopsutil: %v. partition: %v", err, partition)
continue
}
Part.UsedPercent = int(usage.UsedPercent)
partition.UsedPercent = uint32(usage.UsedPercent)
Free, Mag := utils.ConvertBytes(usage.Free)
Part.Free = fmt.Sprintf("%3d%s", uint64(Free), Mag)
bytesFree, magnitudeFree := utils.ConvertBytes(usage.Free)
partition.Free = fmt.Sprintf("%3d%s", uint64(bytesFree), magnitudeFree)
ret, err := psDisk.IOCounters(Part.Device)
ioCounters, err := psDisk.IOCounters(partition.Device)
if err != nil {
log.Printf("failed to get partition read/write info from gopsutil: %v. Part: %v", err, Part)
log.Printf("failed to get partition read/write info from gopsutil: %v. partition: %v", err, partition)
continue
}
data := ret[strings.Replace(Part.Device, "/dev/", "", -1)]
curRead, curWrite := data.ReadBytes, data.WriteBytes
if Part.TotalRead != 0 { // if this isn't the first update
readRecent := curRead - Part.TotalRead
writeRecent := curWrite - Part.TotalWrite
ioCounter := ioCounters[strings.Replace(partition.Device, "/dev/", "", -1)]
bytesRead, bytesWritten := ioCounter.ReadBytes, ioCounter.WriteBytes
if partition.BytesRead != 0 { // if this isn't the first update
bytesReadRecently := bytesRead - partition.BytesRead
bytesWrittenRecently := bytesWritten - partition.BytesWritten
readFloat, unitRead := utils.ConvertBytes(readRecent)
writeFloat, unitWrite := utils.ConvertBytes(writeRecent)
readRecent, writeRecent = uint64(readFloat), uint64(writeFloat)
Part.CurRead = fmt.Sprintf("%d%s", readRecent, unitRead)
Part.CurWrite = fmt.Sprintf("%d%s", writeRecent, unitWrite)
readFloat, readMagnitude := utils.ConvertBytes(bytesReadRecently)
writeFloat, writeMagnitude := utils.ConvertBytes(bytesWrittenRecently)
bytesReadRecently, bytesWrittenRecently = uint64(readFloat), uint64(writeFloat)
partition.BytesReadRecently = fmt.Sprintf("%d%s", bytesReadRecently, readMagnitude)
partition.BytesWrittenRecently = fmt.Sprintf("%d%s", bytesWrittenRecently, writeMagnitude)
} else {
Part.CurRead = fmt.Sprintf("%d%s", 0, "B")
Part.CurWrite = fmt.Sprintf("%d%s", 0, "B")
partition.BytesReadRecently = fmt.Sprintf("%d%s", 0, "B")
partition.BytesWrittenRecently = fmt.Sprintf("%d%s", 0, "B")
}
Part.TotalRead, Part.TotalWrite = curRead, curWrite
partition.BytesRead, partition.BytesWritten = bytesRead, bytesWritten
}
// converts self.Partitions into self.Rows which is a [][]String
sortedPartitions := []string{}
for seriesName := range self.Partitions {
sortedPartitions = append(sortedPartitions, seriesName)
@ -142,31 +149,15 @@ func (self *Disk) update() {
sort.Strings(sortedPartitions)
self.Rows = make([][]string, len(self.Partitions))
for i, key := range sortedPartitions {
Part := self.Partitions[key]
partition := self.Partitions[key]
self.Rows[i] = make([]string, 6)
self.Rows[i][0] = strings.Replace(strings.Replace(Part.Device, "/dev/", "", -1), "mapper/", "", -1)
self.Rows[i][1] = Part.Mount
self.Rows[i][2] = fmt.Sprintf("%d%%", Part.UsedPercent)
self.Rows[i][3] = Part.Free
self.Rows[i][4] = Part.CurRead
self.Rows[i][5] = Part.CurWrite
}
}
// ColResize overrides the default ColResize in the termui table.
func (self *Disk) ColResize() {
self.ColWidths = []int{
utils.Max(4, (self.Inner.Dx()-29)/2),
utils.Max(5, (self.Inner.Dx()-29)/2),
4, 5, 5, 5,
}
self.CellXPos = []int{}
cur := 1
for _, w := range self.ColWidths {
self.CellXPos = append(self.CellXPos, cur)
cur += w
cur += self.Gap
self.Rows[i][0] = strings.Replace(strings.Replace(partition.Device, "/dev/", "", -1), "mapper/", "", -1)
self.Rows[i][1] = partition.MountPoint
self.Rows[i][2] = fmt.Sprintf("%d%%", partition.UsedPercent)
self.Rows[i][3] = partition.Free
self.Rows[i][4] = partition.BytesReadRecently
self.Rows[i][5] = partition.BytesWrittenRecently
}
}

View File

@ -57,9 +57,9 @@ func (self *HelpMenu) Draw(buf *ui.Buffer) {
self.Block.Draw(buf)
for y, line := range strings.Split(KEYBINDS, "\n") {
for x, char := range line {
for x, rune := range line {
buf.SetCell(
ui.NewCell(char, ui.NewStyle(7)),
ui.NewCell(rune, ui.NewStyle(7)),
image.Pt(self.Inner.Min.X+x, self.Inner.Min.Y+y-1),
)
}

View File

@ -12,15 +12,15 @@ import (
"github.com/cjbassi/gotop/src/utils"
)
type Mem struct {
type MemWidget struct {
*ui.LineGraph
interval time.Duration
updateInterval time.Duration
}
func NewMem(renderLock *sync.RWMutex, interval time.Duration, horizontalScale int) *Mem {
self := &Mem{
LineGraph: ui.NewLineGraph(),
interval: interval,
func NewMemWidget(renderLock *sync.RWMutex, updateInterval time.Duration, horizontalScale int) *MemWidget {
self := &MemWidget{
LineGraph: ui.NewLineGraph(),
updateInterval: updateInterval,
}
self.Title = " Memory Usage "
self.HorizontalScale = horizontalScale
@ -30,7 +30,7 @@ func NewMem(renderLock *sync.RWMutex, interval time.Duration, horizontalScale in
self.update()
go func() {
for range time.NewTicker(self.interval).C {
for range time.NewTicker(self.updateInterval).C {
renderLock.RLock()
self.update()
renderLock.RUnlock()
@ -40,24 +40,36 @@ func NewMem(renderLock *sync.RWMutex, interval time.Duration, horizontalScale in
return self
}
func (self *Mem) update() {
main, err := psMem.VirtualMemory()
func (self *MemWidget) update() {
mainMemory, err := psMem.VirtualMemory()
if err != nil {
log.Printf("failed to get main memory info from gopsutil: %v", err)
} else {
self.Data["Main"] = append(self.Data["Main"], main.UsedPercent)
mainTotalBytes, mainTotalMagnitude := utils.ConvertBytes(main.Total)
mainUsedBytes, mainUsedMagnitude := utils.ConvertBytes(main.Used)
self.Labels["Main"] = fmt.Sprintf("%3.0f%% %5.1f%s/%.0f%s", main.UsedPercent, mainUsedBytes, mainUsedMagnitude, mainTotalBytes, mainTotalMagnitude)
self.Data["Main"] = append(self.Data["Main"], mainMemory.UsedPercent)
mainMemoryTotalBytes, mainMemoryTotalMagnitude := utils.ConvertBytes(mainMemory.Total)
mainMemoryUsedBytes, mainMemoryUsedMagnitude := utils.ConvertBytes(mainMemory.Used)
self.Labels["Main"] = fmt.Sprintf("%3.0f%% %5.1f%s/%.0f%s",
mainMemory.UsedPercent,
mainMemoryUsedBytes,
mainMemoryUsedMagnitude,
mainMemoryTotalBytes,
mainMemoryTotalMagnitude,
)
}
swap, err := psMem.SwapMemory()
swapMemory, err := psMem.SwapMemory()
if err != nil {
log.Printf("failed to get swap memory info from gopsutil: %v", err)
} else {
self.Data["Swap"] = append(self.Data["Swap"], swap.UsedPercent)
swapTotalBytes, swapTotalMagnitude := utils.ConvertBytes(swap.Total)
swapUsedBytes, swapUsedMagnitude := utils.ConvertBytes(swap.Used)
self.Labels["Swap"] = fmt.Sprintf("%3.0f%% %5.1f%s/%.0f%s", swap.UsedPercent, swapUsedBytes, swapUsedMagnitude, swapTotalBytes, swapTotalMagnitude)
self.Data["Swap"] = append(self.Data["Swap"], swapMemory.UsedPercent)
swapMemoryTotalBytes, swapMemoryTotalMagnitude := utils.ConvertBytes(swapMemory.Total)
swapMemoryUsedBytes, swapMemoryUsedMagnitude := utils.ConvertBytes(swapMemory.Used)
self.Labels["Swap"] = fmt.Sprintf("%3.0f%% %5.1f%s/%.0f%s",
swapMemory.UsedPercent,
swapMemoryUsedBytes,
swapMemoryUsedMagnitude,
swapMemoryTotalBytes,
swapMemoryTotalMagnitude,
)
}
}

View File

@ -12,33 +12,33 @@ import (
"github.com/cjbassi/gotop/src/utils"
)
type Net struct {
*ui.Sparklines
interval time.Duration
type NetWidget struct {
*ui.SparklineGroup
updateInterval time.Duration
// used to calculate recent network activity
prevRecvTotal uint64
prevSentTotal uint64
totalBytesRecv uint64
totalBytesSent uint64
}
func NewNet(renderLock *sync.RWMutex) *Net {
recv := ui.NewSparkline()
recv.Data = []int{}
func NewNetWidget(renderLock *sync.RWMutex) *NetWidget {
recvSparkline := ui.NewSparkline()
recvSparkline.Data = []int{}
sent := ui.NewSparkline()
sent.Data = []int{}
sentSparkline := ui.NewSparkline()
sentSparkline.Data = []int{}
spark := ui.NewSparklines(recv, sent)
self := &Net{
Sparklines: spark,
interval: time.Second,
spark := ui.NewSparklineGroup(recvSparkline, sentSparkline)
self := &NetWidget{
SparklineGroup: spark,
updateInterval: time.Second,
}
self.Title = " Network Usage "
self.update()
go func() {
for range time.NewTicker(self.interval).C {
for range time.NewTicker(self.updateInterval).C {
renderLock.RLock()
self.update()
renderLock.RUnlock()
@ -48,60 +48,62 @@ func NewNet(renderLock *sync.RWMutex) *Net {
return self
}
func (self *Net) update() {
func (self *NetWidget) update() {
interfaces, err := psNet.IOCounters(true)
if err != nil {
log.Printf("failed to get network activity from gopsutil: %v", err)
return
}
var curRecvTotal uint64
var curSentTotal uint64
var totalBytesRecv uint64
var totalBytesSent uint64
for _, _interface := range interfaces {
// ignore VPN interface
if _interface.Name != "tun0" {
curRecvTotal += _interface.BytesRecv
curSentTotal += _interface.BytesSent
totalBytesRecv += _interface.BytesRecv
totalBytesSent += _interface.BytesSent
}
}
var recvRecent uint64
var sentRecent uint64
if self.prevRecvTotal != 0 { // if this isn't the first update
recvRecent = curRecvTotal - self.prevRecvTotal
sentRecent = curSentTotal - self.prevSentTotal
var recentBytesRecv uint64
var recentBytesSent uint64
if int(recvRecent) < 0 {
log.Printf("error: negative value for recently received network data from gopsutil. recvRecent: %v", recvRecent)
if self.totalBytesRecv != 0 { // if this isn't the first update
recentBytesRecv = totalBytesRecv - self.totalBytesRecv
recentBytesSent = totalBytesSent - self.totalBytesSent
if int(recentBytesRecv) < 0 {
log.Printf("error: negative value for recently received network data from gopsutil. recentBytesRecv: %v", recentBytesRecv)
// recover from error
recvRecent = 0
recentBytesRecv = 0
}
if int(sentRecent) < 0 {
log.Printf("error: negative value for recently sent network data from gopsutil. sentRecent: %v", sentRecent)
if int(recentBytesSent) < 0 {
log.Printf("error: negative value for recently sent network data from gopsutil. recentBytesSent: %v", recentBytesSent)
// recover from error
sentRecent = 0
recentBytesSent = 0
}
self.Lines[0].Data = append(self.Lines[0].Data, int(recvRecent))
self.Lines[1].Data = append(self.Lines[1].Data, int(sentRecent))
self.Lines[0].Data = append(self.Lines[0].Data, int(recentBytesRecv))
self.Lines[1].Data = append(self.Lines[1].Data, int(recentBytesSent))
}
// used in later calls to update
self.prevRecvTotal = curRecvTotal
self.prevSentTotal = curSentTotal
self.totalBytesRecv = totalBytesRecv
self.totalBytesSent = totalBytesSent
// render widget titles
for i := 0; i < 2; i++ {
total, label, recent := func() (uint64, string, uint64) {
if i == 0 {
return curRecvTotal, "RX", recvRecent
return totalBytesRecv, "RX", recentBytesRecv
}
return curSentTotal, "Tx", sentRecent
return totalBytesSent, "Tx", recentBytesSent
}()
recentConv, unitRecent := utils.ConvertBytes(uint64(recent))
totalConv, unitTotal := utils.ConvertBytes(uint64(total))
recentConverted, unitRecent := utils.ConvertBytes(uint64(recent))
totalConverted, unitTotal := utils.ConvertBytes(uint64(total))
self.Lines[i].Title1 = fmt.Sprintf(" Total %s: %5.1f %s", label, totalConv, unitTotal)
self.Lines[i].Title2 = fmt.Sprintf(" %s/s: %9.1f %2s/s", label, recentConv, unitRecent)
self.Lines[i].Title1 = fmt.Sprintf(" Total %s: %5.1f %s", label, totalConverted, unitTotal)
self.Lines[i].Title2 = fmt.Sprintf(" %s/s: %9.1f %2s/s", label, recentConverted, unitRecent)
}
}

View File

@ -9,7 +9,6 @@ import (
"sync"
"time"
"github.com/gizak/termui"
psCPU "github.com/shirou/gopsutil/cpu"
ui "github.com/cjbassi/gotop/src/termui"
@ -17,56 +16,67 @@ import (
)
const (
UP = "▲"
DOWN = "▼"
UP_ARROW = "▲"
DOWN_ARROW = "▼"
)
// Process represents each process.
type Process struct {
PID int
Command string
CPU float64
Mem float64
Args string
}
type ProcSortMethod string
const (
ProcSortCpu ProcSortMethod = "c"
ProcSortMem = "m"
ProcSortPid = "p"
)
type Proc struct {
*ui.Table
cpuCount float64
interval time.Duration
sortMethod string
groupedProcs []Process
ungroupedProcs []Process
group bool
Pid int
CommandName string
FullCommand string
Cpu float64
Mem float64
}
func NewProc(renderLock *sync.RWMutex) *Proc {
type ProcWidget struct {
*ui.Table
cpuCount float64
updateInterval time.Duration
sortMethod ProcSortMethod
groupedProcs []Proc
ungroupedProcs []Proc
showGroupedProcs bool
}
func NewProcWidget(renderLock *sync.RWMutex) *ProcWidget {
cpuCount, err := psCPU.Counts(false)
if err != nil {
log.Printf("failed to get CPU count from gopsutil: %v", err)
}
self := &Proc{
Table: ui.NewTable(),
interval: time.Second,
cpuCount: float64(cpuCount),
sortMethod: "c",
group: true,
self := &ProcWidget{
Table: ui.NewTable(),
updateInterval: time.Second,
cpuCount: float64(cpuCount),
sortMethod: ProcSortCpu,
showGroupedProcs: true,
}
self.Title = " Processes "
self.ColResizer = self.ColResize
self.Cursor = true
self.Gap = 3
self.ShowCursor = true
self.ColGap = 3
self.PadLeft = 2
self.ColResizer = func() {
self.ColWidths = []int{
5, utils.MaxInt(self.Inner.Dx()-26, 10), 4, 4,
}
}
self.UniqueCol = 0
if self.group {
if self.showGroupedProcs {
self.UniqueCol = 1
}
self.update()
go func() {
for range time.NewTicker(self.interval).C {
for range time.NewTicker(self.updateInterval).C {
renderLock.RLock()
self.update()
renderLock.RUnlock()
@ -76,120 +86,104 @@ func NewProc(renderLock *sync.RWMutex) *Proc {
return self
}
// Sort sorts either the grouped or ungrouped []Process based on the sortMethod.
func (self *ProcWidget) update() {
procs, err := getProcs()
if err != nil {
log.Printf("failed to retrieve processes: %v", err)
return
}
// can't iterate on the entries directly since we can't modify them that way
for i := range procs {
procs[i].Cpu /= self.cpuCount
}
self.ungroupedProcs = procs
self.groupedProcs = groupProcs(procs)
self.sortProcs()
self.convertProcsToTableRows()
}
// sortProcs sorts either the grouped or ungrouped []Process based on the sortMethod.
// Called with every update, when the sort method is changed, and when processes are grouped and ungrouped.
func (self *Proc) Sort() {
func (self *ProcWidget) sortProcs() {
self.Header = []string{"Count", "Command", "CPU%", "Mem%"}
if !self.group {
if !self.showGroupedProcs {
self.Header[0] = "PID"
}
processes := &self.ungroupedProcs
if self.group {
processes = &self.groupedProcs
var procs *[]Proc
if self.showGroupedProcs {
procs = &self.groupedProcs
} else {
procs = &self.ungroupedProcs
}
switch self.sortMethod {
case "c":
sort.Sort(sort.Reverse(ProcessByCPU(*processes)))
self.Header[2] += DOWN
case "p":
if self.group {
sort.Sort(sort.Reverse(ProcessByPID(*processes)))
case ProcSortCpu:
sort.Sort(sort.Reverse(SortProcsByCpu(*procs)))
self.Header[2] += DOWN_ARROW
case ProcSortPid:
if self.showGroupedProcs {
sort.Sort(sort.Reverse(SortProcsByPid(*procs)))
} else {
sort.Sort(ProcessByPID(*processes))
sort.Sort(SortProcsByPid(*procs))
}
self.Header[0] += DOWN
case "m":
sort.Sort(sort.Reverse(ProcessByMem(*processes)))
self.Header[3] += DOWN
}
self.Rows = FieldsToStrings(*processes, self.group)
}
// ColResize overrides the default ColResize in the termui table.
func (self *Proc) ColResize() {
self.ColWidths = []int{
5, utils.Max(self.Inner.Dx()-26, 10), 4, 4,
self.Header[0] += DOWN_ARROW
case ProcSortMem:
sort.Sort(sort.Reverse(SortProcsByMem(*procs)))
self.Header[3] += DOWN_ARROW
}
}
func (self *Proc) ChangeSort(e termui.Event) {
if self.sortMethod != e.ID {
self.sortMethod = e.ID
self.Top()
self.Sort()
// convertProcsToTableRows converts a []Proc to a [][]string and sets it to the table Rows
func (self *ProcWidget) convertProcsToTableRows() {
var procs *[]Proc
if self.showGroupedProcs {
procs = &self.groupedProcs
} else {
procs = &self.ungroupedProcs
}
strings := make([][]string, len(*procs))
for i := range *procs {
strings[i] = make([]string, 4)
strings[i][0] = strconv.Itoa(int((*procs)[i].Pid))
if self.showGroupedProcs {
strings[i][1] = (*procs)[i].CommandName
} else {
strings[i][1] = (*procs)[i].FullCommand
}
strings[i][2] = fmt.Sprintf("%4s", strconv.FormatFloat((*procs)[i].Cpu, 'f', 1, 64))
strings[i][3] = fmt.Sprintf("%4s", strconv.FormatFloat(float64((*procs)[i].Mem), 'f', 1, 64))
}
self.Rows = strings
}
func (self *ProcWidget) ChangeProcSortMethod(method ProcSortMethod) {
if self.sortMethod != method {
self.sortMethod = method
self.ScrollTop()
self.sortProcs()
self.convertProcsToTableRows()
}
}
func (self *Proc) Tab() {
self.group = !self.group
if self.group {
func (self *ProcWidget) ToggleShowingGroupedProcs() {
self.showGroupedProcs = !self.showGroupedProcs
if self.showGroupedProcs {
self.UniqueCol = 1
} else {
self.UniqueCol = 0
}
self.Sort()
self.Top()
self.ScrollTop()
self.sortProcs()
self.convertProcsToTableRows()
}
// 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 {
groupedP := make(map[string]Process)
for _, process := range P {
val, ok := groupedP[process.Command]
if ok {
groupedP[process.Command] = Process{
val.PID + 1,
val.Command,
val.CPU + process.CPU,
val.Mem + process.Mem,
"",
}
} else {
groupedP[process.Command] = Process{
1,
process.Command,
process.CPU,
process.Mem,
"",
}
}
}
groupList := make([]Process, len(groupedP))
var i int
for _, val := range groupedP {
groupList[i] = val
i++
}
return groupList
}
// FieldsToStrings converts a []Process to a [][]string
func FieldsToStrings(P []Process, grouped bool) [][]string {
strings := make([][]string, len(P))
for i, p := range P {
strings[i] = make([]string, 4)
strings[i][0] = strconv.Itoa(int(p.PID))
if grouped {
strings[i][1] = p.Command
} else {
strings[i][1] = p.Args
}
strings[i][2] = fmt.Sprintf("%4s", strconv.FormatFloat(p.CPU, 'f', 1, 64))
strings[i][3] = fmt.Sprintf("%4s", strconv.FormatFloat(float64(p.Mem), 'f', 1, 64))
}
return strings
}
// Kill kills process or group of processes.
func (self *Proc) Kill() {
// KillProc kills a process or group of processes depending on if we're displaying the processes grouped or not.
func (self *ProcWidget) KillProc() {
self.SelectedItem = ""
command := "kill"
if self.UniqueCol == 1 {
@ -200,57 +194,91 @@ func (self *Proc) Kill() {
cmd.Wait()
}
/////////////////////////////////////////////////////////////////////////////////
// []Process Sorting //
/////////////////////////////////////////////////////////////////////////////////
// groupProcs groupes a []Proc based on command name.
// The first field changes from PID to count.
// Cpu and Mem are added together for each Proc.
func groupProcs(procs []Proc) []Proc {
groupedProcsMap := make(map[string]Proc)
for _, proc := range procs {
val, ok := groupedProcsMap[proc.CommandName]
if ok {
groupedProcsMap[proc.CommandName] = Proc{
val.Pid + 1,
val.CommandName,
"",
val.Cpu + proc.Cpu,
val.Mem + proc.Mem,
}
} else {
groupedProcsMap[proc.CommandName] = Proc{
1,
proc.CommandName,
"",
proc.Cpu,
proc.Mem,
}
}
}
type ProcessByCPU []Process
groupedProcsList := make([]Proc, len(groupedProcsMap))
i := 0
for _, val := range groupedProcsMap {
groupedProcsList[i] = val
i++
}
return groupedProcsList
}
// []Proc Sorting //////////////////////////////////////////////////////////////
type SortProcsByCpu []Proc
// Len implements Sort interface
func (P ProcessByCPU) Len() int {
return len(P)
func (self SortProcsByCpu) Len() int {
return len(self)
}
// Swap implements Sort interface
func (P ProcessByCPU) Swap(i, j int) {
P[i], P[j] = P[j], P[i]
func (self SortProcsByCpu) Swap(i, j int) {
self[i], self[j] = self[j], self[i]
}
// Less implements Sort interface
func (P ProcessByCPU) Less(i, j int) bool {
return P[i].CPU < P[j].CPU
func (self SortProcsByCpu) Less(i, j int) bool {
return self[i].Cpu < self[j].Cpu
}
type ProcessByPID []Process
type SortProcsByPid []Proc
// Len implements Sort interface
func (P ProcessByPID) Len() int {
return len(P)
func (self SortProcsByPid) Len() int {
return len(self)
}
// Swap implements Sort interface
func (P ProcessByPID) Swap(i, j int) {
P[i], P[j] = P[j], P[i]
func (self SortProcsByPid) Swap(i, j int) {
self[i], self[j] = self[j], self[i]
}
// Less implements Sort interface
func (P ProcessByPID) Less(i, j int) bool {
return P[i].PID < P[j].PID
func (self SortProcsByPid) Less(i, j int) bool {
return self[i].Pid < self[j].Pid
}
type ProcessByMem []Process
type SortProcsByMem []Proc
// Len implements Sort interface
func (P ProcessByMem) Len() int {
return len(P)
func (self SortProcsByMem) Len() int {
return len(self)
}
// Swap implements Sort interface
func (P ProcessByMem) Swap(i, j int) {
P[i], P[j] = P[j], P[i]
func (self SortProcsByMem) Swap(i, j int) {
self[i], self[j] = self[j], self[i]
}
// Less implements Sort interface
func (P ProcessByMem) Less(i, j int) bool {
return P[i].Mem < P[j].Mem
func (self SortProcsByMem) Less(i, j int) bool {
return self[i].Mem < self[j].Mem
}

View File

@ -8,35 +8,17 @@ import (
"strings"
)
func (self *Proc) update() {
processes, err := Processes()
if err != nil {
log.Printf("failed to retrieve processes: %v", err)
return
}
// have to iterate like this in order to actually change the value
for i := range processes {
processes[i].CPU /= self.cpuCount
}
self.ungroupedProcs = processes
self.groupedProcs = Group(processes)
self.Sort()
}
func Processes() ([]Process, error) {
func getProcs() ([]Proc, error) {
output, err := exec.Command("ps", "-axo", "pid:10,comm:50,pcpu:5,pmem:5,args").Output()
if err != nil {
return nil, fmt.Errorf("failed to execute 'ps' command: %v", err)
}
// converts to []string, removing trailing newline and header
processStrArr := strings.Split(strings.TrimSuffix(string(output), "\n"), "\n")[1:]
linesOfProcStrings := strings.Split(strings.TrimSuffix(string(output), "\n"), "\n")[1:]
processes := []Process{}
for _, line := range processStrArr {
procs := []Proc{}
for _, line := range linesOfProcStrings {
pid, err := strconv.Atoi(strings.TrimSpace(line[0:10]))
if err != nil {
log.Printf("failed to convert PID to int: %v. line: %v", err, line)
@ -49,14 +31,15 @@ func Processes() ([]Process, error) {
if err != nil {
log.Printf("failed to convert Mem usage to float: %v. line: %v", err, line)
}
process := Process{
PID: pid,
Command: strings.TrimSpace(line[11:61]),
CPU: cpu,
Mem: mem,
Args: line[74:],
proc := Proc{
Pid: pid,
CommandName: strings.TrimSpace(line[11:61]),
FullCommand: line[74:],
Cpu: cpu,
Mem: mem,
}
processes = append(processes, process)
procs = append(procs, proc)
}
return processes, nil
return procs, nil
}

View File

@ -11,31 +11,31 @@ import (
)
const (
// Define column widths for ps output used in Processes()
// Define column widths for ps output used in Procs()
five = "12345"
ten = five + five
fifty = ten + ten + ten + ten + ten
)
func (self *Proc) update() {
processes, err := Processes()
func (self *ProcWidget) update() {
procs, err := GetProcs()
if err != nil {
log.Printf("failed to retrieve processes: %v", err)
return
}
// have to iterate like this in order to actually change the value
for i := range processes {
processes[i].CPU /= self.cpuCount
for i := range procs {
procs[i].CPU /= self.cpuCount
}
self.ungroupedProcs = processes
self.groupedProcs = Group(processes)
self.ungroupedProcs = procs
self.groupedProcs = GroupProcs(procs)
self.Sort()
self.SortProcesses()
}
func Processes() ([]Process, error) {
func GetProcs() ([]Process, error) {
keywords := fmt.Sprintf("pid=%s,comm=%s,pcpu=%s,pmem=%s,args", ten, fifty, five, five)
output, err := exec.Command("ps", "-caxo", keywords).Output()
if err != nil {
@ -43,10 +43,10 @@ func Processes() ([]Process, error) {
}
// converts to []string and removes the header
strOutput := strings.Split(strings.TrimSpace(string(output)), "\n")[1:]
linesOfProcStrings := strings.Split(strings.TrimSpace(string(output)), "\n")[1:]
processes := []Process{}
for _, line := range strOutput {
procs := []Proc{}
for _, line := range linesOfProcStrings {
pid, err := strconv.Atoi(strings.TrimSpace(line[0:10]))
if err != nil {
log.Printf("failed to convert first field to int: %v. split: %v", err, line)
@ -59,14 +59,15 @@ func Processes() ([]Process, error) {
if err != nil {
log.Printf("failed to convert fourth field to float: %v. split: %v", err, line)
}
process := Process{
proc := Proc{
PID: pid,
Command: strings.TrimSpace(line[11:61]),
CPU: cpu,
Mem: mem,
Args: line[74:],
}
processes = append(processes, process)
procs = append(procs, proc)
}
return processes, nil
return procs, nil
}

View File

@ -1,34 +1,35 @@
package widgets
import (
"fmt"
"log"
psProc "github.com/shirou/gopsutil/process"
)
func (self *Proc) update() {
psProcesses, err := psProc.Processes()
func getProcs() ([]Proc, error) {
psProcs, err := psProc.Processes()
if err != nil {
log.Printf("failed to get processes from gopsutil: %v", err)
return
return nil, fmt.Errorf("failed to get processes from gopsutil: %v", err)
}
processes := make([]Process, len(psProcesses))
for i, psProcess := range psProcesses {
pid := psProcess.Pid
command, err := psProcess.Name()
procs := make([]Proc, len(psProcs))
for i, psProc := range psProcs {
pid := psProc.Pid
command, err := psProc.Name()
if err != nil {
log.Printf("failed to get process command from gopsutil: %v. psProcess: %v. i: %v. pid: %v", err, psProcess, i, pid)
log.Printf("failed to get process command from gopsutil: %v. psProc: %v. i: %v. pid: %v", err, psProc, i, pid)
}
cpu, err := psProcess.CPUPercent()
cpu, err := psProc.CPUPercent()
if err != nil {
log.Printf("failed to get process cpu usage from gopsutil: %v. psProcess: %v. i: %v. pid: %v", err, psProcess, i, pid)
log.Printf("failed to get process cpu usage from gopsutil: %v. psProc: %v. i: %v. pid: %v", err, psProc, i, pid)
}
mem, err := psProcess.MemoryPercent()
mem, err := psProc.MemoryPercent()
if err != nil {
log.Printf("failed to get process memeory usage from gopsutil: %v. psProcess: %v. i: %v. pid: %v", err, psProcess, i, pid)
log.Printf("failed to get process memeory usage from gopsutil: %v. psProc: %v. i: %v. pid: %v", err, psProc, i, pid)
}
processes[i] = Process{
procs[i] = Proc{
int(pid),
command,
cpu / self.cpuCount,
@ -39,8 +40,5 @@ func (self *Proc) update() {
}
}
self.ungroupedProcs = processes
self.groupedProcs = Group(processes)
self.Sort()
return procs, nil
}

View File

@ -2,6 +2,7 @@ package widgets
import (
"image"
"log"
"os"
"time"
@ -21,27 +22,31 @@ func NewStatusBar() *StatusBar {
func (self *StatusBar) Draw(buf *ui.Buffer) {
self.Block.Draw(buf)
hostname, _ := os.Hostname()
hostname, err := os.Hostname()
if err != nil {
log.Printf("could not get hostname: %v", err)
return
}
buf.SetString(
hostname,
ui.NewStyle(7),
ui.NewStyle(ui.ColorWhite),
image.Pt(self.Inner.Min.X, self.Inner.Min.Y+(self.Inner.Dy()/2)),
)
t := time.Now()
_time := t.Format("15:04:05")
currentTime := time.Now()
formattedTime := currentTime.Format("15:04:05")
buf.SetString(
_time,
ui.NewStyle(7),
formattedTime,
ui.NewStyle(ui.ColorWhite),
image.Pt(
self.Inner.Min.X+(self.Inner.Dx()/2)-len(_time)/2,
self.Inner.Min.X+(self.Inner.Dx()/2)-len(formattedTime)/2,
self.Inner.Min.Y+(self.Inner.Dy()/2),
),
)
buf.SetString(
"gotop",
ui.NewStyle(7),
ui.NewStyle(ui.ColorWhite),
image.Pt(
self.Inner.Max.X-6,
self.Inner.Min.Y+(self.Inner.Dy()/2),

View File

@ -8,36 +8,45 @@ import (
"time"
ui "github.com/gizak/termui"
"github.com/cjbassi/gotop/src/utils"
)
type Temp struct {
*ui.Block // inherits from Block instead of a premade Widget
interval time.Duration
Data map[string]int
Threshold int
TempLow ui.Color
TempHigh ui.Color
Fahrenheit bool
type TempScale int
const (
Celcius TempScale = 0
Fahrenheit = 1
)
type TempWidget struct {
*ui.Block // inherits from Block instead of a premade Widget
updateInterval time.Duration
Data map[string]int
TempThreshold int
TempLowColor ui.Color
TempHighColor ui.Color
TempScale TempScale
}
func NewTemp(renderLock *sync.RWMutex, fahrenheit bool) *Temp {
self := &Temp{
Block: ui.NewBlock(),
interval: time.Second * 5,
Data: make(map[string]int),
Threshold: 80, // temp at which color should change
func NewTempWidget(renderLock *sync.RWMutex, tempScale TempScale) *TempWidget {
self := &TempWidget{
Block: ui.NewBlock(),
updateInterval: time.Second * 5,
Data: make(map[string]int),
TempThreshold: 80,
TempScale: tempScale,
}
self.Title = " Temperatures "
if fahrenheit {
self.Fahrenheit = true
self.Threshold = int(self.Threshold*9/5 + 32)
if tempScale == Fahrenheit {
self.TempThreshold = utils.CelsiusToFahrenheit(self.TempThreshold)
}
self.update()
go func() {
for range time.NewTicker(self.interval).C {
for range time.NewTicker(self.updateInterval).C {
renderLock.RLock()
self.update()
renderLock.RUnlock()
@ -47,8 +56,8 @@ func NewTemp(renderLock *sync.RWMutex, fahrenheit bool) *Temp {
return self
}
// We implement a custom Draw method instead of inheriting from a generic Widget.
func (self *Temp) Draw(buf *ui.Buffer) {
// Custom Draw method instead of inheriting from a generic Widget.
func (self *TempWidget) Draw(buf *ui.Buffer) {
self.Block.Draw(buf)
var keys []string
@ -62,9 +71,11 @@ func (self *Temp) Draw(buf *ui.Buffer) {
break
}
fg := self.TempLow
if self.Data[key] >= self.Threshold {
fg = self.TempHigh
var fg ui.Color
if self.Data[key] < self.TempThreshold {
fg = self.TempLowColor
} else {
fg = self.TempHighColor
}
s := ui.TrimString(key, (self.Inner.Dx() - 4))
@ -72,13 +83,15 @@ func (self *Temp) Draw(buf *ui.Buffer) {
ui.Theme.Default,
image.Pt(self.Inner.Min.X, self.Inner.Min.Y+y),
)
if self.Fahrenheit {
switch self.TempScale {
case Fahrenheit:
buf.SetString(
fmt.Sprintf("%3dF", self.Data[key]),
ui.NewStyle(fg),
image.Pt(self.Inner.Max.X-4, self.Inner.Min.Y+y),
)
} else {
case Celcius:
buf.SetString(
fmt.Sprintf("%3dC", self.Data[key]),
ui.NewStyle(fg),

View File

@ -53,7 +53,7 @@ func SensorsTemperatures() ([]TemperatureStat, error) {
return temperatures, nil
}
func (self *Temp) update() {
func (self *TempWidget) update() {
sensors, err := SensorsTemperatures()
if err != nil {
log.Printf("failed to get sensors from CGO: %v", err)
@ -61,9 +61,10 @@ func (self *Temp) update() {
}
for _, sensor := range sensors {
if sensor.Temperature != 0 {
if self.Fahrenheit {
switch self.TempScale {
case Fahrenheit:
self.Data[sensor.SensorKey] = utils.CelsiusToFahrenheit(int(sensor.Temperature))
} else {
case Celcius:
self.Data[sensor.SensorKey] = int(sensor.Temperature)
}
}

View File

@ -1,5 +1,7 @@
package widgets
// loosely based on https://github.com/openbsd/src/blob/master/sbin/sysctl/sysctl.c#L2517
// #include <sys/time.h>
// #include <sys/sysctl.h>
// #include <sys/sensors.h>
@ -13,14 +15,14 @@ import (
"github.com/cjbassi/gotop/src/utils"
)
func getTemp(t *Temp, mib []C.int, mlen int, snsrdev *C.struct_sensordev, index int) {
func (self *TempWidget) getTemp(mib []C.int, mlen int, snsrdev *C.struct_sensordev, index int) {
switch mlen {
case 4:
k := mib[3]
var numt C.int
for numt = 0; numt < snsrdev.maxnumt[k]; numt++ {
mib[4] = numt
getTemp(t, mib, mlen+1, snsrdev, int(numt))
self.getTemp(mib, mlen+1, snsrdev, int(numt))
}
case 5:
var snsr C.struct_sensor
@ -34,16 +36,17 @@ func getTemp(t *Temp, mib []C.int, mlen int, snsrdev *C.struct_sensordev, index
key := C.GoString(&snsrdev.xname[0]) + ".temp" + strconv.Itoa(index)
temp := int((snsr.value - 273150000.0) / 1000000.0)
if t.Fahrenheit {
t.Data[key] = utils.CelsiusToFahrenheit(temp)
} else {
t.Data[key] = temp
switch self.TempScale {
case Fahrenheit:
self.Data[key] = utils.CelsiusToFahrenheit(temp)
case Celcius:
self.Data[key] = temp
}
}
}
}
func (self *Temp) update() {
func (self *TempWidget) update() {
mib := []C.int{0, 1, 2, 3, 4}
var snsrdev C.struct_sensordev
@ -56,17 +59,14 @@ func (self *Temp) update() {
var i C.int
for i = 0; ; i++ {
mib[2] = i
if v, e := C.sysctl(&mib[0], 3, unsafe.Pointer(&snsrdev), &len, nil, 0); v == -1 {
if e == syscall.ENXIO {
continue
}
if e == syscall.ENOENT {
break
}
}
getTemp(self, mib, 4, &snsrdev, 0)
self.getTemp(mib, 4, &snsrdev, 0)
}
}

View File

@ -11,7 +11,7 @@ import (
"github.com/cjbassi/gotop/src/utils"
)
func (self *Temp) update() {
func (self *TempWidget) update() {
sensors, err := psHost.SensorsTemperatures()
if err != nil {
log.Printf("error recieved from gopsutil: %v", err)
@ -21,9 +21,10 @@ func (self *Temp) update() {
if strings.Contains(sensor.SensorKey, "input") && sensor.Temperature != 0 {
// removes '_input' from the end of the sensor name
label := sensor.SensorKey[:strings.Index(sensor.SensorKey, "_input")]
if self.Fahrenheit {
switch self.TempScale {
case Fahrenheit:
self.Data[label] = utils.CelsiusToFahrenheit(int(sensor.Temperature))
} else {
case Celcius:
self.Data[label] = int(sensor.Temperature)
}
}

View File

@ -8,7 +8,7 @@ import (
"github.com/cjbassi/gotop/src/utils"
)
func (self *Temp) update() {
func (self *TempWidget) update() {
sensors, err := psHost.SensorsTemperatures()
if err != nil {
log.Printf("failed to get sensors from gopsutil: %v", err)
@ -16,9 +16,10 @@ func (self *Temp) update() {
}
for _, sensor := range sensors {
if sensor.Temperature != 0 {
if self.Fahrenheit {
switch self.TempScale {
case Fahrenheit:
self.Data[sensor.SensorKey] = utils.CelsiusToFahrenheit(int(sensor.Temperature))
} else {
case Celcius:
self.Data[sensor.SensorKey] = int(sensor.Temperature)
}
}