11// Copyright (c) 2024 Carsen Klock under MIT License
22// mactop is a simple terminal based Apple Silicon power monitor written in Go Lang!
3+ // _
4+ // _ __ __ _ __| |_ ___ _ __
5+ // | ' \/ _` / _| _/ _ \ '_ \
6+ // |_|_|_\__,_\__|\__\___/ .__/
7+ // |_|
38
49package main
510
611import (
712 "bufio"
813 "fmt"
914 "log"
15+ "math"
1016 "os"
1117 "os/exec"
1218 "os/signal"
@@ -15,6 +21,7 @@ import (
1521 "strconv"
1622 "strings"
1723 "syscall"
24+ "time"
1825
1926 ui "github.com/gizak/termui/v3"
2027 w "github.com/gizak/termui/v3/widgets"
@@ -78,11 +85,13 @@ type MemoryMetrics struct {
7885
7986var (
8087 cpu1Gauge , cpu2Gauge , gpuGauge , aneGauge * w.Gauge
81- TotalPowerChart * w.Plot
88+ TotalPowerChart * w.BarChart
8289 memoryGauge * w.Gauge
8390 modelText , PowerChart , NetworkInfo , ProcessInfo * w.Paragraph
8491 grid * ui.Grid
8592
93+ powerValues []float64
94+ lastUpdateTime time.Time
8695 stderrLogger = log .New (os .Stderr , "" , 0 )
8796 currentGridLayout = "default"
8897 updateInterval = 1000
@@ -154,12 +163,16 @@ func setupUI() {
154163 ProcessInfo = w .NewParagraph ()
155164 ProcessInfo .Title = "Process Info"
156165
157- TotalPowerChart = w .NewPlot ()
158- TotalPowerChart .Title = "Total Power Usage (W)"
159- TotalPowerChart .Data = make ([][]float64 , 1 )
160- TotalPowerChart .Data [0 ] = []float64 {1 , 2 , 3 , 4 , 5 }
161- TotalPowerChart .AxesColor = ui .ColorGreen
162- TotalPowerChart .LineColors = []ui.Color {ui .ColorCyan }
166+ TotalPowerChart = w .NewBarChart ()
167+ TotalPowerChart .Title = "~ W Total Power"
168+ TotalPowerChart .SetRect (50 , 0 , 75 , 10 )
169+ TotalPowerChart .BarWidth = 5 // Adjust the bar width to fill the available space
170+ TotalPowerChart .BarGap = 1 // Remove the gap between the bars
171+ TotalPowerChart .PaddingBottom = 0
172+ TotalPowerChart .PaddingTop = 1
173+ TotalPowerChart .NumFormatter = func (num float64 ) string {
174+ return ""
175+ }
163176
164177 memoryGauge = w .NewGauge ()
165178 memoryGauge .Title = "Memory Usage"
@@ -251,6 +264,7 @@ func main() {
251264 fmt .Println ("--help: Show this help message" )
252265 fmt .Println ("--version: Show the version of mactop" )
253266 fmt .Println ("--interval: Set the powermetrics update interval in milliseconds. Default is 1000." )
267+ fmt .Println ("--color: Set the UI color. Default is white. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'. (-c green)" )
254268 fmt .Println ("You must use sudo to run mactop, as powermetrics requires root privileges." )
255269 fmt .Println ("For more information, see https://github.com/context-labs/mactop" )
256270 os .Exit (0 )
@@ -274,6 +288,35 @@ func main() {
274288 os .Exit (1 )
275289 }
276290
291+ if len (os .Args ) > 2 && os .Args [1 ] == "--color" || len (os .Args ) > 2 && os .Args [1 ] == "-c" {
292+ colorName := strings .ToLower (os .Args [2 ])
293+ var color ui.Color
294+ switch colorName {
295+ case "green" :
296+ color = ui .ColorGreen
297+ case "red" :
298+ color = ui .ColorRed
299+ case "blue" :
300+ color = ui .ColorBlue
301+ case "cyan" :
302+ color = ui .ColorCyan
303+ case "magenta" :
304+ color = ui .ColorMagenta
305+ case "yellow" :
306+ color = ui .ColorYellow
307+ case "white" :
308+ color = ui .ColorWhite
309+ default :
310+ stderrLogger .Printf ("Unsupported color: %s. Using default color.\n " , colorName )
311+ color = ui .ColorWhite
312+ }
313+ ui .Theme .Block .Title .Fg = color
314+ ui .Theme .Block .Border .Fg = color
315+ ui .Theme .Paragraph .Text .Fg = color
316+ ui .Theme .BarChart .Bars = []ui.Color {color }
317+ ui .Theme .Gauge .Label .Fg = color
318+ }
319+
277320 if len (os .Args ) > 1 && os .Args [1 ] == "--interval" || len (os .Args ) > 1 && os .Args [1 ] == "-i" {
278321 interval , err := strconv .Atoi (os .Args [2 ])
279322 if err != nil {
@@ -312,7 +355,7 @@ func main() {
312355 signal .Notify (quit , os .Interrupt , syscall .SIGTERM )
313356
314357 go collectMetrics (done , cpuMetricsChan , gpuMetricsChan , netdiskMetricsChan , processMetricsChan )
315-
358+ lastUpdateTime = time . Now ()
316359 go func () {
317360 for {
318361 select {
@@ -375,21 +418,15 @@ func main() {
375418}
376419
377420func setupLogfile () (* os.File , error ) {
378- // create the log directory
379421 if err := os .MkdirAll ("logs" , 0755 ); err != nil {
380422 return nil , fmt .Errorf ("failed to make the log directory: %v" , err )
381423 }
382- // open the log file
383424 logfile , err := os .OpenFile ("logs/mactop.log" , os .O_CREATE | os .O_WRONLY | os .O_TRUNC , 0660 )
384425 if err != nil {
385426 return nil , fmt .Errorf ("failed to open log file: %v" , err )
386427 }
387-
388- // log time, filename, and line number
389428 log .SetFlags (log .Ltime | log .Lshortfile )
390- // log to file
391429 log .SetOutput (logfile )
392-
393430 return logfile , nil
394431}
395432
@@ -445,21 +482,22 @@ func collectMetrics(done chan struct{}, cpumetricsChan chan CPUMetrics, gpumetri
445482}
446483
447484func updateTotalPowerChart (newPowerValue float64 ) {
448- // stderrLogger.Printf("Rendering TotalPowerChart with data: %v\n", TotalPowerChart.Data)
449- if len (TotalPowerChart .Data [0 ]) == 0 {
450- TotalPowerChart .Data [0 ] = append (TotalPowerChart .Data [0 ], 0 ) // Ensure there's at least one data point
451- }
452-
453- TotalPowerChart .Data [0 ] = append (TotalPowerChart .Data [0 ], newPowerValue )
454-
455- if len (TotalPowerChart .Data [0 ]) > 250 {
456- TotalPowerChart .Data [0 ] = TotalPowerChart .Data [0 ][1 :]
457- }
458-
459- if len (TotalPowerChart .Data [0 ]) > 0 {
485+ currentTime := time .Now ()
486+ powerValues = append (powerValues , newPowerValue )
487+ if currentTime .Sub (lastUpdateTime ) >= 2 * time .Second {
488+ var sum float64
489+ for _ , value := range powerValues {
490+ sum += value
491+ }
492+ averagePower := sum / float64 (len (powerValues ))
493+ averagePower = math .Round (averagePower )
494+ TotalPowerChart .Data = append ([]float64 {averagePower }, TotalPowerChart .Data ... )
495+ if len (TotalPowerChart .Data ) > 25 {
496+ TotalPowerChart .Data = TotalPowerChart .Data [:25 ]
497+ }
498+ powerValues = nil
499+ lastUpdateTime = currentTime
460500 ui .Render (TotalPowerChart )
461- } else {
462- log .Println ("No data to render for TotalPowerChart" )
463501 }
464502}
465503
@@ -477,7 +515,6 @@ func updateCPUUI(cpuMetrics CPUMetrics) {
477515 PowerChart .Text = fmt .Sprintf ("CPU Power: %.1f W\n GPU Power: %.1f W\n ANE Power: %.1f W\n Total Power: %.1f W" , cpuMetrics .CPUW , cpuMetrics .GPUW , cpuMetrics .ANEW , cpuMetrics .PackageW )
478516
479517 memoryMetrics := getMemoryMetrics ()
480-
481518 memoryGauge .Title = fmt .Sprintf ("Memory Usage: %.2f GB / %.2f GB (Swap: %.2f/%.2f GB)" , float64 (memoryMetrics .Used )/ 1024 / 1024 / 1024 , float64 (memoryMetrics .Total )/ 1024 / 1024 / 1024 , float64 (memoryMetrics .SwapUsed )/ 1024 / 1024 / 1024 , float64 (memoryMetrics .SwapTotal )/ 1024 / 1024 / 1024 )
482519 memoryGauge .Percent = int ((float64 (memoryMetrics .Used ) / float64 (memoryMetrics .Total )) * 100 )
483520
@@ -536,7 +573,6 @@ func parseProcessMetrics(powermetricsOutput string, processMetrics []ProcessMetr
536573 sort .Slice (processMetrics , func (i , j int ) bool {
537574 return processMetrics [i ].CPUUsage > processMetrics [j ].CPUUsage
538575 })
539-
540576 return processMetrics
541577}
542578
@@ -689,7 +725,6 @@ func parseCPUMetrics(powermetricsOutput string, cpuMetrics CPUMetrics) CPUMetric
689725 cpuMetrics .EClusterActive = (cpuMetrics .E0ClusterActive + cpuMetrics .E1ClusterActive ) / 2
690726 cpuMetrics .EClusterFreqMHz = max (cpuMetrics .E0ClusterFreqMHz , cpuMetrics .E1ClusterFreqMHz )
691727 }
692-
693728 if cpuMetrics .P3ClusterActive != 0 {
694729 // M1 Ultra
695730 cpuMetrics .PClusterActive = (cpuMetrics .P0ClusterActive + cpuMetrics .P1ClusterActive + cpuMetrics .P2ClusterActive + cpuMetrics .P3ClusterActive ) / 4
@@ -780,7 +815,6 @@ func getSOCInfo() map[string]interface{} {
780815 "gpu_core_count" : getGPUCores (),
781816 }
782817
783- // TDP (power)
784818 switch socInfo ["name" ] {
785819 case "Apple M1 Max" :
786820 socInfo ["cpu_max_power" ] = 30
@@ -802,7 +836,6 @@ func getSOCInfo() map[string]interface{} {
802836 socInfo ["gpu_max_power" ] = 20
803837 }
804838
805- // Bandwidth
806839 switch socInfo ["name" ] {
807840 case "Apple M1 Max" :
808841 socInfo ["cpu_max_bw" ] = 250
0 commit comments