1
1
// Copyright (c) 2024 Carsen Klock under MIT License
2
2
// mactop is a simple terminal based Apple Silicon power monitor written in Go Lang!
3
+ // _
4
+ // _ __ __ _ __| |_ ___ _ __
5
+ // | ' \/ _` / _| _/ _ \ '_ \
6
+ // |_|_|_\__,_\__|\__\___/ .__/
7
+ // |_|
3
8
4
9
package main
5
10
6
11
import (
7
12
"bufio"
8
13
"fmt"
9
14
"log"
15
+ "math"
10
16
"os"
11
17
"os/exec"
12
18
"os/signal"
@@ -15,6 +21,7 @@ import (
15
21
"strconv"
16
22
"strings"
17
23
"syscall"
24
+ "time"
18
25
19
26
ui "github.com/gizak/termui/v3"
20
27
w "github.com/gizak/termui/v3/widgets"
@@ -78,11 +85,13 @@ type MemoryMetrics struct {
78
85
79
86
var (
80
87
cpu1Gauge , cpu2Gauge , gpuGauge , aneGauge * w.Gauge
81
- TotalPowerChart * w.Plot
88
+ TotalPowerChart * w.BarChart
82
89
memoryGauge * w.Gauge
83
90
modelText , PowerChart , NetworkInfo , ProcessInfo * w.Paragraph
84
91
grid * ui.Grid
85
92
93
+ powerValues []float64
94
+ lastUpdateTime time.Time
86
95
stderrLogger = log .New (os .Stderr , "" , 0 )
87
96
currentGridLayout = "default"
88
97
updateInterval = 1000
@@ -154,12 +163,16 @@ func setupUI() {
154
163
ProcessInfo = w .NewParagraph ()
155
164
ProcessInfo .Title = "Process Info"
156
165
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
+ }
163
176
164
177
memoryGauge = w .NewGauge ()
165
178
memoryGauge .Title = "Memory Usage"
@@ -251,6 +264,7 @@ func main() {
251
264
fmt .Println ("--help: Show this help message" )
252
265
fmt .Println ("--version: Show the version of mactop" )
253
266
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)" )
254
268
fmt .Println ("You must use sudo to run mactop, as powermetrics requires root privileges." )
255
269
fmt .Println ("For more information, see https://github.com/context-labs/mactop" )
256
270
os .Exit (0 )
@@ -274,6 +288,35 @@ func main() {
274
288
os .Exit (1 )
275
289
}
276
290
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
+
277
320
if len (os .Args ) > 1 && os .Args [1 ] == "--interval" || len (os .Args ) > 1 && os .Args [1 ] == "-i" {
278
321
interval , err := strconv .Atoi (os .Args [2 ])
279
322
if err != nil {
@@ -312,7 +355,7 @@ func main() {
312
355
signal .Notify (quit , os .Interrupt , syscall .SIGTERM )
313
356
314
357
go collectMetrics (done , cpuMetricsChan , gpuMetricsChan , netdiskMetricsChan , processMetricsChan )
315
-
358
+ lastUpdateTime = time . Now ()
316
359
go func () {
317
360
for {
318
361
select {
@@ -375,21 +418,15 @@ func main() {
375
418
}
376
419
377
420
func setupLogfile () (* os.File , error ) {
378
- // create the log directory
379
421
if err := os .MkdirAll ("logs" , 0755 ); err != nil {
380
422
return nil , fmt .Errorf ("failed to make the log directory: %v" , err )
381
423
}
382
- // open the log file
383
424
logfile , err := os .OpenFile ("logs/mactop.log" , os .O_CREATE | os .O_WRONLY | os .O_TRUNC , 0660 )
384
425
if err != nil {
385
426
return nil , fmt .Errorf ("failed to open log file: %v" , err )
386
427
}
387
-
388
- // log time, filename, and line number
389
428
log .SetFlags (log .Ltime | log .Lshortfile )
390
- // log to file
391
429
log .SetOutput (logfile )
392
-
393
430
return logfile , nil
394
431
}
395
432
@@ -445,21 +482,22 @@ func collectMetrics(done chan struct{}, cpumetricsChan chan CPUMetrics, gpumetri
445
482
}
446
483
447
484
func 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
460
500
ui .Render (TotalPowerChart )
461
- } else {
462
- log .Println ("No data to render for TotalPowerChart" )
463
501
}
464
502
}
465
503
@@ -477,7 +515,6 @@ func updateCPUUI(cpuMetrics CPUMetrics) {
477
515
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 )
478
516
479
517
memoryMetrics := getMemoryMetrics ()
480
-
481
518
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 )
482
519
memoryGauge .Percent = int ((float64 (memoryMetrics .Used ) / float64 (memoryMetrics .Total )) * 100 )
483
520
@@ -536,7 +573,6 @@ func parseProcessMetrics(powermetricsOutput string, processMetrics []ProcessMetr
536
573
sort .Slice (processMetrics , func (i , j int ) bool {
537
574
return processMetrics [i ].CPUUsage > processMetrics [j ].CPUUsage
538
575
})
539
-
540
576
return processMetrics
541
577
}
542
578
@@ -689,7 +725,6 @@ func parseCPUMetrics(powermetricsOutput string, cpuMetrics CPUMetrics) CPUMetric
689
725
cpuMetrics .EClusterActive = (cpuMetrics .E0ClusterActive + cpuMetrics .E1ClusterActive ) / 2
690
726
cpuMetrics .EClusterFreqMHz = max (cpuMetrics .E0ClusterFreqMHz , cpuMetrics .E1ClusterFreqMHz )
691
727
}
692
-
693
728
if cpuMetrics .P3ClusterActive != 0 {
694
729
// M1 Ultra
695
730
cpuMetrics .PClusterActive = (cpuMetrics .P0ClusterActive + cpuMetrics .P1ClusterActive + cpuMetrics .P2ClusterActive + cpuMetrics .P3ClusterActive ) / 4
@@ -780,7 +815,6 @@ func getSOCInfo() map[string]interface{} {
780
815
"gpu_core_count" : getGPUCores (),
781
816
}
782
817
783
- // TDP (power)
784
818
switch socInfo ["name" ] {
785
819
case "Apple M1 Max" :
786
820
socInfo ["cpu_max_power" ] = 30
@@ -802,7 +836,6 @@ func getSOCInfo() map[string]interface{} {
802
836
socInfo ["gpu_max_power" ] = 20
803
837
}
804
838
805
- // Bandwidth
806
839
switch socInfo ["name" ] {
807
840
case "Apple M1 Max" :
808
841
socInfo ["cpu_max_bw" ] = 250
0 commit comments