73
73
powerHistory = make ([]float64 , 100 )
74
74
maxPower = 0.0 // Track maximum power for better scaling
75
75
gpuValues = make ([]float64 , 100 )
76
+ headless = false
76
77
prometheusPort string
77
78
)
78
79
@@ -926,6 +927,13 @@ func cycleColors() {
926
927
ui .Render (processList )
927
928
}
928
929
930
+ func block () {
931
+ for {
932
+ fmt .Printf ("%v+\n " , time .Now ())
933
+ time .Sleep (time .Second * 3600 )
934
+ }
935
+ }
936
+
929
937
func main () {
930
938
var (
931
939
colorName string
@@ -936,11 +944,13 @@ func main() {
936
944
for i := 1 ; i < len (os .Args ); i ++ {
937
945
switch os .Args [i ] {
938
946
case "--help" , "-h" :
939
- fmt .Print ("Usage: mactop [--help] [--version] [--interval] [--color]\n --help: Show this help message\n --version: Show the version of mactop\n --interval: Set the powermetrics update interval in milliseconds. Default is 1000.\n --color: Set the UI color. Default is none. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'. (-c green)\n \n You must use sudo to run mactop, as powermetrics requires root privileges.\n \n For more information, see https://github.com/context-labs/mactop written by Carsen Klock.\n " )
947
+ fmt .Print ("Usage: mactop [--help] [--version] [--interval] [--color] [--headless] \n --help: Show this help message\n --version: Show the version of mactop\n --interval: Set the powermetrics update interval in milliseconds. Default is 1000.\n --color: Set the UI color. Default is none. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'. (-c green)\n --headless: Run without any UI \n \n You must use sudo to run mactop, as powermetrics requires root privileges.\n \n For more information, see https://github.com/context-labs/mactop written by Carsen Klock.\n " )
940
948
os .Exit (0 )
941
949
case "--version" , "-v" :
942
950
fmt .Println ("mactop version:" , version )
943
951
os .Exit (0 )
952
+ case "--headless" , "-l" :
953
+ headless = true
944
954
case "--test" , "-t" :
945
955
if i + 1 < len (os .Args ) {
946
956
testInput := os .Args [i + 1 ]
@@ -990,17 +1000,20 @@ func main() {
990
1000
}
991
1001
defer logfile .Close ()
992
1002
993
- if err := ui .Init (); err != nil {
994
- stderrLogger .Fatalf ("failed to initialize termui: %v" , err )
1003
+ if ! headless {
1004
+ if err := ui .Init (); err != nil {
1005
+ stderrLogger .Fatalf ("failed to initialize termui: %v" , err )
1006
+ }
1007
+ defer ui .Close ()
995
1008
}
996
- defer ui . Close ()
1009
+
997
1010
StderrToLogfile (logfile )
998
1011
999
1012
if prometheusPort != "" {
1000
1013
startPrometheusServer (prometheusPort )
1001
1014
stderrLogger .Printf ("Prometheus metrics available at http://localhost:%s/metrics\n " , prometheusPort )
1002
1015
}
1003
- if setColor {
1016
+ if setColor && ! headless {
1004
1017
var color ui.Color
1005
1018
switch colorName {
1006
1019
case "green" :
@@ -1027,19 +1040,24 @@ func main() {
1027
1040
cpuGauge .BarColor , gpuGauge .BarColor , memoryGauge .BarColor = color , color , color
1028
1041
processList .TextStyle = ui .NewStyle (color )
1029
1042
processList .SelectedRowStyle = ui .NewStyle (ui .ColorBlack , color )
1030
- } else {
1043
+ } else if ! headless {
1031
1044
setupUI ()
1032
1045
}
1033
1046
if setInterval {
1034
1047
updateInterval = interval
1035
1048
}
1036
- setupGrid ()
1037
- termWidth , termHeight := ui .TerminalDimensions ()
1038
- grid .SetRect (0 , 0 , termWidth , termHeight )
1049
+
1050
+ if ! headless {
1051
+ setupGrid ()
1052
+ termWidth , termHeight := ui .TerminalDimensions ()
1053
+ grid .SetRect (0 , 0 , termWidth , termHeight )
1054
+ }
1055
+
1039
1056
cpuMetricsChan := make (chan CPUMetrics , 1 )
1040
1057
gpuMetricsChan := make (chan GPUMetrics , 1 )
1041
1058
netdiskMetricsChan := make (chan NetDiskMetrics , 1 )
1042
1059
go collectMetrics (done , cpuMetricsChan , gpuMetricsChan , netdiskMetricsChan )
1060
+
1043
1061
go func () {
1044
1062
ticker := time .NewTicker (time .Duration (updateInterval ) * time .Millisecond )
1045
1063
defer ticker .Stop ()
@@ -1048,14 +1066,20 @@ func main() {
1048
1066
case cpuMetrics := <- cpuMetricsChan :
1049
1067
updateCPUUI (cpuMetrics )
1050
1068
updateTotalPowerChart (cpuMetrics .PackageW )
1051
- ui . Render ( grid )
1069
+ render ( )
1052
1070
case gpuMetrics := <- gpuMetricsChan :
1053
1071
updateGPUUI (gpuMetrics )
1054
- ui . Render ( grid )
1072
+ render ( )
1055
1073
case netdiskMetrics := <- netdiskMetricsChan :
1074
+ if headless {
1075
+ continue
1076
+ }
1056
1077
updateNetDiskUI (netdiskMetrics )
1057
- ui . Render ( grid )
1078
+ render ( )
1058
1079
case <- ticker .C :
1080
+ if headless {
1081
+ continue
1082
+ }
1059
1083
percentages , err := GetCPUPercentages ()
1060
1084
if err != nil {
1061
1085
stderrLogger .Printf ("Error getting CPU percentages: %v\n " , err )
@@ -1075,13 +1099,13 @@ func main() {
1075
1099
totalUsage ,
1076
1100
)
1077
1101
updateProcessList ()
1078
- ui . Render ( grid )
1102
+ render ( )
1079
1103
case <- done :
1080
1104
return
1081
1105
}
1082
1106
}
1083
1107
}()
1084
- ui . Render ( grid )
1108
+ render ( )
1085
1109
done := make (chan struct {})
1086
1110
quit := make (chan os.Signal , 1 )
1087
1111
signal .Notify (quit , os .Interrupt , syscall .SIGTERM )
@@ -1091,63 +1115,84 @@ func main() {
1091
1115
}
1092
1116
}()
1093
1117
lastUpdateTime = time .Now ()
1094
- uiEvents := ui .PollEvents ()
1095
- for {
1096
- select {
1097
- case e := <- uiEvents :
1098
- handleProcessListEvents (e )
1099
- switch e .ID {
1100
- case "q" , "<C-c>" :
1101
- close (done )
1118
+
1119
+ // Block main goroutine until program is manually stopped
1120
+ if headless {
1121
+ go block ()
1122
+ quitChannel := make (chan os.Signal , 1 )
1123
+ signal .Notify (quitChannel , syscall .SIGINT , syscall .SIGTERM )
1124
+ <- quitChannel
1125
+ //time for cleanup before exit
1126
+ fmt .Println ("Adios!" )
1127
+ os .Exit (0 )
1128
+ }
1129
+
1130
+ if ! headless {
1131
+ uiEvents := ui .PollEvents ()
1132
+ for {
1133
+ select {
1134
+ case e := <- uiEvents :
1135
+ handleProcessListEvents (e )
1136
+ switch e .ID {
1137
+ case "q" , "<C-c>" :
1138
+ close (done )
1139
+ ui .Close ()
1140
+ os .Exit (0 )
1141
+ return
1142
+ case "<Resize>" :
1143
+ payload := e .Payload .(ui.Resize )
1144
+ grid .SetRect (0 , 0 , payload .Width , payload .Height )
1145
+ render ()
1146
+ case "r" :
1147
+ termWidth , termHeight := ui .TerminalDimensions ()
1148
+ grid .SetRect (0 , 0 , termWidth , termHeight )
1149
+ ui .Clear ()
1150
+ render ()
1151
+ case "p" :
1152
+ togglePartyMode ()
1153
+ case "c" :
1154
+ termWidth , termHeight := ui .TerminalDimensions ()
1155
+ grid .SetRect (0 , 0 , termWidth , termHeight )
1156
+ cycleColors ()
1157
+ ui .Clear ()
1158
+ render ()
1159
+ case "l" :
1160
+ termWidth , termHeight := ui .TerminalDimensions ()
1161
+ grid .SetRect (0 , 0 , termWidth , termHeight )
1162
+ ui .Clear ()
1163
+ switchGridLayout ()
1164
+ render ()
1165
+ case "h" , "?" :
1166
+ termWidth , termHeight := ui .TerminalDimensions ()
1167
+ grid .SetRect (0 , 0 , termWidth , termHeight )
1168
+ ui .Clear ()
1169
+ toggleHelpMenu ()
1170
+ render ()
1171
+ case "j" , "<Down>" :
1172
+ if selectedProcess < len (processList .Rows )- 1 {
1173
+ selectedProcess ++
1174
+ ui .Render (processList )
1175
+ }
1176
+ case "k" , "<Up>" :
1177
+ if selectedProcess > 0 {
1178
+ selectedProcess --
1179
+ ui .Render (processList )
1180
+ }
1181
+ }
1182
+ case <- done :
1102
1183
ui .Close ()
1103
1184
os .Exit (0 )
1104
1185
return
1105
- case "<Resize>" :
1106
- payload := e .Payload .(ui.Resize )
1107
- grid .SetRect (0 , 0 , payload .Width , payload .Height )
1108
- ui .Render (grid )
1109
- case "r" :
1110
- termWidth , termHeight := ui .TerminalDimensions ()
1111
- grid .SetRect (0 , 0 , termWidth , termHeight )
1112
- ui .Clear ()
1113
- ui .Render (grid )
1114
- case "p" :
1115
- togglePartyMode ()
1116
- case "c" :
1117
- termWidth , termHeight := ui .TerminalDimensions ()
1118
- grid .SetRect (0 , 0 , termWidth , termHeight )
1119
- cycleColors ()
1120
- ui .Clear ()
1121
- ui .Render (grid )
1122
- case "l" :
1123
- termWidth , termHeight := ui .TerminalDimensions ()
1124
- grid .SetRect (0 , 0 , termWidth , termHeight )
1125
- ui .Clear ()
1126
- switchGridLayout ()
1127
- ui .Render (grid )
1128
- case "h" , "?" :
1129
- termWidth , termHeight := ui .TerminalDimensions ()
1130
- grid .SetRect (0 , 0 , termWidth , termHeight )
1131
- ui .Clear ()
1132
- toggleHelpMenu ()
1133
- ui .Render (grid )
1134
- case "j" , "<Down>" :
1135
- if selectedProcess < len (processList .Rows )- 1 {
1136
- selectedProcess ++
1137
- ui .Render (processList )
1138
- }
1139
- case "k" , "<Up>" :
1140
- if selectedProcess > 0 {
1141
- selectedProcess --
1142
- ui .Render (processList )
1143
- }
1144
1186
}
1145
- case <- done :
1146
- ui .Close ()
1147
- os .Exit (0 )
1148
- return
1149
1187
}
1150
1188
}
1189
+
1190
+ }
1191
+
1192
+ func render () {
1193
+ if ! headless {
1194
+ ui .Render (grid )
1195
+ }
1151
1196
}
1152
1197
1153
1198
func setupLogfile () (* os.File , error ) {
@@ -1360,10 +1405,14 @@ func updateTotalPowerChart(watts float64) {
1360
1405
if count > 0 {
1361
1406
avgWatts = sum / float64 (count )
1362
1407
}
1363
- sparkline .Data = powerValues
1364
- sparkline .MaxVal = 8 // Match MaxHeight
1365
- sparklineGroup .Title = fmt .Sprintf ("%.2f W Total (Max: %.2f W)" , watts , maxPowerSeen )
1366
- sparkline .Title = fmt .Sprintf ("Avg: %.2f W" , avgWatts )
1408
+
1409
+ if ! headless {
1410
+ sparkline .Data = powerValues
1411
+ sparkline .MaxVal = 8 // Match MaxHeight
1412
+ sparklineGroup .Title = fmt .Sprintf ("%.2f W Total (Max: %.2f W)" , watts , maxPowerSeen )
1413
+ sparkline .Title = fmt .Sprintf ("Avg: %.2f W" , avgWatts )
1414
+ }
1415
+
1367
1416
}
1368
1417
1369
1418
func updateCPUUI (cpuMetrics CPUMetrics ) {
@@ -1372,38 +1421,48 @@ func updateCPUUI(cpuMetrics CPUMetrics) {
1372
1421
stderrLogger .Printf ("Error getting CPU percentages: %v\n " , err )
1373
1422
return
1374
1423
}
1375
- cpuCoreWidget .UpdateUsage (coreUsages )
1424
+ if ! headless {
1425
+ cpuCoreWidget .UpdateUsage (coreUsages )
1426
+ }
1427
+
1376
1428
var totalUsage float64
1377
1429
for _ , usage := range coreUsages {
1378
1430
totalUsage += usage
1379
1431
}
1380
1432
totalUsage /= float64 (len (coreUsages ))
1381
- cpuGauge .Percent = int (totalUsage )
1382
- cpuGauge .Title = fmt .Sprintf ("mactop - %d Cores (%dE/%dP) - CPU Usage: %.2f%%" ,
1383
- cpuCoreWidget .eCoreCount + cpuCoreWidget .pCoreCount ,
1384
- cpuCoreWidget .eCoreCount ,
1385
- cpuCoreWidget .pCoreCount ,
1386
- totalUsage ,
1387
- )
1388
- cpuCoreWidget .Title = fmt .Sprintf ("mactop - %d Cores (%dE/%dP) %.2f%%" ,
1389
- cpuCoreWidget .eCoreCount + cpuCoreWidget .pCoreCount ,
1390
- cpuCoreWidget .eCoreCount ,
1391
- cpuCoreWidget .pCoreCount ,
1392
- totalUsage ,
1393
- )
1394
- PowerChart .Title = fmt .Sprintf ("%.2f W CPU - %.2f W GPU" , cpuMetrics .CPUW , cpuMetrics .GPUW )
1395
- PowerChart .Text = fmt .Sprintf ("CPU Power: %.2f W\n GPU Power: %.2f W\n Total Power: %.2f W\n Thermals: %s" ,
1396
- cpuMetrics .CPUW ,
1397
- cpuMetrics .GPUW ,
1398
- cpuMetrics .PackageW ,
1399
- map [bool ]string {
1400
- true : "Throttled!" ,
1401
- false : "Nominal" ,
1402
- }[cpuMetrics .Throttled ],
1403
- )
1433
+
1434
+ if ! headless {
1435
+ cpuGauge .Percent = int (totalUsage )
1436
+ cpuGauge .Title = fmt .Sprintf ("mactop - %d Cores (%dE/%dP) - CPU Usage: %.2f%%" ,
1437
+ cpuCoreWidget .eCoreCount + cpuCoreWidget .pCoreCount ,
1438
+ cpuCoreWidget .eCoreCount ,
1439
+ cpuCoreWidget .pCoreCount ,
1440
+ totalUsage ,
1441
+ )
1442
+ cpuCoreWidget .Title = fmt .Sprintf ("mactop - %d Cores (%dE/%dP) %.2f%%" ,
1443
+ cpuCoreWidget .eCoreCount + cpuCoreWidget .pCoreCount ,
1444
+ cpuCoreWidget .eCoreCount ,
1445
+ cpuCoreWidget .pCoreCount ,
1446
+ totalUsage ,
1447
+ )
1448
+ PowerChart .Title = fmt .Sprintf ("%.2f W CPU - %.2f W GPU" , cpuMetrics .CPUW , cpuMetrics .GPUW )
1449
+ PowerChart .Text = fmt .Sprintf ("CPU Power: %.2f W\n GPU Power: %.2f W\n Total Power: %.2f W\n Thermals: %s" ,
1450
+ cpuMetrics .CPUW ,
1451
+ cpuMetrics .GPUW ,
1452
+ cpuMetrics .PackageW ,
1453
+ map [bool ]string {
1454
+ true : "Throttled!" ,
1455
+ false : "Nominal" ,
1456
+ }[cpuMetrics .Throttled ],
1457
+ )
1458
+ }
1459
+
1404
1460
memoryMetrics := getMemoryMetrics ()
1405
- 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 )
1406
- memoryGauge .Percent = int ((float64 (memoryMetrics .Used ) / float64 (memoryMetrics .Total )) * 100 )
1461
+
1462
+ if ! headless {
1463
+ 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 )
1464
+ memoryGauge .Percent = int ((float64 (memoryMetrics .Used ) / float64 (memoryMetrics .Total )) * 100 )
1465
+ }
1407
1466
1408
1467
cpuUsage .Set (float64 (totalUsage ))
1409
1468
powerUsage .With (prometheus.Labels {"component" : "cpu" }).Set (cpuMetrics .CPUW )
@@ -1417,8 +1476,10 @@ func updateCPUUI(cpuMetrics CPUMetrics) {
1417
1476
}
1418
1477
1419
1478
func updateGPUUI (gpuMetrics GPUMetrics ) {
1420
- gpuGauge .Title = fmt .Sprintf ("GPU Usage: %d%% @ %d MHz" , int (gpuMetrics .Active ), gpuMetrics .FreqMHz )
1421
- gpuGauge .Percent = int (gpuMetrics .Active )
1479
+ if ! headless {
1480
+ gpuGauge .Title = fmt .Sprintf ("GPU Usage: %d%% @ %d MHz" , int (gpuMetrics .Active ), gpuMetrics .FreqMHz )
1481
+ gpuGauge .Percent = int (gpuMetrics .Active )
1482
+ }
1422
1483
1423
1484
// Add GPU history tracking
1424
1485
for i := 0 ; i < len (gpuValues )- 1 ; i ++ {
@@ -1440,9 +1501,11 @@ func updateGPUUI(gpuMetrics GPUMetrics) {
1440
1501
avgGPU = sum / float64 (count )
1441
1502
}
1442
1503
1443
- gpuSparkline .Data = gpuValues
1444
- gpuSparkline .MaxVal = 100 // GPU usage is 0-100%
1445
- gpuSparklineGroup .Title = fmt .Sprintf ("GPU History: %d%% (Avg: %.1f%%)" , gpuMetrics .Active , avgGPU )
1504
+ if ! headless {
1505
+ gpuSparkline .Data = gpuValues
1506
+ gpuSparkline .MaxVal = 100 // GPU usage is 0-100%
1507
+ gpuSparklineGroup .Title = fmt .Sprintf ("GPU History: %d%% (Avg: %.1f%%)" , gpuMetrics .Active , avgGPU )
1508
+ }
1446
1509
1447
1510
gpuUsage .Set (float64 (gpuMetrics .Active ))
1448
1511
gpuFreqMHz .Set (float64 (gpuMetrics .FreqMHz ))
0 commit comments