diff --git a/go.sum b/go.sum index cca1ebd..b27eb47 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index a21e11d..8cc62ce 100644 --- a/main.go +++ b/main.go @@ -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 "": payload := e.Payload.(ui.Mouse) - proc.Click(payload.X, payload.Y) + proc.HandleClick(payload.X, payload.Y) render(proc) case "k", "", "": - proc.Up() + proc.ScrollUp() render(proc) case "j", "", "": - proc.Down() + proc.ScrollDown() render(proc) case "": - proc.Top() + proc.ScrollTop() render(proc) case "g": if previousKey == "g" { - proc.Top() + proc.ScrollTop() render(proc) } case "G", "": - proc.Bottom() + proc.ScrollBottom() render(proc) case "": - proc.HalfPageDown() + proc.ScrollHalfPageDown() render(proc) case "": - proc.HalfPageUp() + proc.ScrollHalfPageUp() render(proc) case "": - proc.PageDown() + proc.ScrollPageDown() render(proc) case "": - proc.PageUp() + proc.ScrollPageUp() render(proc) case "d": if previousKey == "d" { - proc.Kill() + proc.KillProc() } case "": - proc.Tab() + proc.ToggleShowingGroupedProcs() render(proc) case "m", "c", "p": - proc.ChangeSort(e) + proc.ChangeProcSortMethod(w.ProcSortMethod(e.ID)) render(proc) } diff --git a/src/logging/logging_arm64.go b/src/logging/logging_arm64.go index 877a4b3..2b7a25f 100644 --- a/src/logging/logging_arm64.go +++ b/src/logging/logging_arm64.go @@ -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) } diff --git a/src/logging/logging_other.go b/src/logging/logging_other.go index aba2aa7..382baca 100644 --- a/src/logging/logging_other.go +++ b/src/logging/logging_other.go @@ -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) } diff --git a/src/termui/linegraph.go b/src/termui/linegraph.go index 1096224..2d0c8e0 100644 --- a/src/termui/linegraph.go +++ b/src/termui/linegraph.go @@ -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 } diff --git a/src/termui/sparkline.go b/src/termui/sparkline.go index 00dddac..85ca919 100644 --- a/src/termui/sparkline.go +++ b/src/termui/sparkline.go @@ -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 diff --git a/src/termui/table.go b/src/termui/table.go index 0b8c71e..57cd9e2 100644 --- a/src/termui/table.go +++ b/src/termui/table.go @@ -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()) { diff --git a/src/utils/utils.go b/src/utils/utils.go index fe2a4a9..7a8b5bd 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -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 } diff --git a/src/widgets/battery.go b/src/widgets/battery.go index 2d59849..b48ca28 100644 --- a/src/widgets/battery.go +++ b/src/widgets/battery.go @@ -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)) } } diff --git a/src/widgets/cpu.go b/src/widgets/cpu.go index 3ee1b23..405ad8b 100644 --- a/src/widgets/cpu.go +++ b/src/widgets/cpu.go @@ -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) } } } diff --git a/src/widgets/disk.go b/src/widgets/disk.go index 6365eac..3d44e31 100644 --- a/src/widgets/disk.go +++ b/src/widgets/disk.go @@ -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 } } diff --git a/src/widgets/help.go b/src/widgets/help.go index 4825c5b..8900603 100644 --- a/src/widgets/help.go +++ b/src/widgets/help.go @@ -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), ) } diff --git a/src/widgets/mem.go b/src/widgets/mem.go index 4bd2695..3f0277b 100644 --- a/src/widgets/mem.go +++ b/src/widgets/mem.go @@ -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, + ) } } diff --git a/src/widgets/net.go b/src/widgets/net.go index 6f0ac4f..abc865a 100644 --- a/src/widgets/net.go +++ b/src/widgets/net.go @@ -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) } } diff --git a/src/widgets/proc.go b/src/widgets/proc.go index d0dbce9..a1e37cc 100644 --- a/src/widgets/proc.go +++ b/src/widgets/proc.go @@ -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 } diff --git a/src/widgets/proc_linux.go b/src/widgets/proc_linux.go index 4163391..0045234 100644 --- a/src/widgets/proc_linux.go +++ b/src/widgets/proc_linux.go @@ -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 } diff --git a/src/widgets/proc_other.go b/src/widgets/proc_other.go index c5f42d5..9d3b965 100644 --- a/src/widgets/proc_other.go +++ b/src/widgets/proc_other.go @@ -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 } diff --git a/src/widgets/proc_windows.go b/src/widgets/proc_windows.go index f486244..1363c22 100644 --- a/src/widgets/proc_windows.go +++ b/src/widgets/proc_windows.go @@ -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 } diff --git a/src/widgets/statusbar.go b/src/widgets/statusbar.go index 5d36fd2..27ba548 100644 --- a/src/widgets/statusbar.go +++ b/src/widgets/statusbar.go @@ -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), diff --git a/src/widgets/temp.go b/src/widgets/temp.go index 9b90069..1162c84 100644 --- a/src/widgets/temp.go +++ b/src/widgets/temp.go @@ -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), diff --git a/src/widgets/temp_darwin.go b/src/widgets/temp_darwin.go index c1ef665..28046a1 100644 --- a/src/widgets/temp_darwin.go +++ b/src/widgets/temp_darwin.go @@ -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) } } diff --git a/src/widgets/temp_openbsd.go b/src/widgets/temp_openbsd.go index 646cae9..8665c03 100644 --- a/src/widgets/temp_openbsd.go +++ b/src/widgets/temp_openbsd.go @@ -1,5 +1,7 @@ package widgets +// loosely based on https://github.com/openbsd/src/blob/master/sbin/sysctl/sysctl.c#L2517 + // #include // #include // #include @@ -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) } } diff --git a/src/widgets/temp_other.go b/src/widgets/temp_other.go index 68995d2..47b37cf 100644 --- a/src/widgets/temp_other.go +++ b/src/widgets/temp_other.go @@ -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) } } diff --git a/src/widgets/temp_windows.go b/src/widgets/temp_windows.go index 8ff5ef4..a1cd47c 100644 --- a/src/widgets/temp_windows.go +++ b/src/widgets/temp_windows.go @@ -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) } }