7373 powerHistory = make ([]float64 , 100 )
7474 maxPower = 0.0 // Track maximum power for better scaling
7575 gpuValues = make ([]float64 , 100 )
76+ headless = false
7677 prometheusPort string
7778)
7879
@@ -926,6 +927,13 @@ func cycleColors() {
926927 ui .Render (processList )
927928}
928929
930+ func block () {
931+ for {
932+ fmt .Printf ("%v+\n " , time .Now ())
933+ time .Sleep (time .Second * 3600 )
934+ }
935+ }
936+
929937func main () {
930938 var (
931939 colorName string
@@ -936,11 +944,13 @@ func main() {
936944 for i := 1 ; i < len (os .Args ); i ++ {
937945 switch os .Args [i ] {
938946 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 " )
940948 os .Exit (0 )
941949 case "--version" , "-v" :
942950 fmt .Println ("mactop version:" , version )
943951 os .Exit (0 )
952+ case "--headless" , "-l" :
953+ headless = true
944954 case "--test" , "-t" :
945955 if i + 1 < len (os .Args ) {
946956 testInput := os .Args [i + 1 ]
@@ -990,17 +1000,20 @@ func main() {
9901000 }
9911001 defer logfile .Close ()
9921002
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 ()
9951008 }
996- defer ui . Close ()
1009+
9971010 StderrToLogfile (logfile )
9981011
9991012 if prometheusPort != "" {
10001013 startPrometheusServer (prometheusPort )
10011014 stderrLogger .Printf ("Prometheus metrics available at http://localhost:%s/metrics\n " , prometheusPort )
10021015 }
1003- if setColor {
1016+ if setColor && ! headless {
10041017 var color ui.Color
10051018 switch colorName {
10061019 case "green" :
@@ -1027,19 +1040,24 @@ func main() {
10271040 cpuGauge .BarColor , gpuGauge .BarColor , memoryGauge .BarColor = color , color , color
10281041 processList .TextStyle = ui .NewStyle (color )
10291042 processList .SelectedRowStyle = ui .NewStyle (ui .ColorBlack , color )
1030- } else {
1043+ } else if ! headless {
10311044 setupUI ()
10321045 }
10331046 if setInterval {
10341047 updateInterval = interval
10351048 }
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+
10391056 cpuMetricsChan := make (chan CPUMetrics , 1 )
10401057 gpuMetricsChan := make (chan GPUMetrics , 1 )
10411058 netdiskMetricsChan := make (chan NetDiskMetrics , 1 )
10421059 go collectMetrics (done , cpuMetricsChan , gpuMetricsChan , netdiskMetricsChan )
1060+
10431061 go func () {
10441062 ticker := time .NewTicker (time .Duration (updateInterval ) * time .Millisecond )
10451063 defer ticker .Stop ()
@@ -1048,14 +1066,20 @@ func main() {
10481066 case cpuMetrics := <- cpuMetricsChan :
10491067 updateCPUUI (cpuMetrics )
10501068 updateTotalPowerChart (cpuMetrics .PackageW )
1051- ui . Render ( grid )
1069+ render ( )
10521070 case gpuMetrics := <- gpuMetricsChan :
10531071 updateGPUUI (gpuMetrics )
1054- ui . Render ( grid )
1072+ render ( )
10551073 case netdiskMetrics := <- netdiskMetricsChan :
1074+ if headless {
1075+ continue
1076+ }
10561077 updateNetDiskUI (netdiskMetrics )
1057- ui . Render ( grid )
1078+ render ( )
10581079 case <- ticker .C :
1080+ if headless {
1081+ continue
1082+ }
10591083 percentages , err := GetCPUPercentages ()
10601084 if err != nil {
10611085 stderrLogger .Printf ("Error getting CPU percentages: %v\n " , err )
@@ -1075,13 +1099,13 @@ func main() {
10751099 totalUsage ,
10761100 )
10771101 updateProcessList ()
1078- ui . Render ( grid )
1102+ render ( )
10791103 case <- done :
10801104 return
10811105 }
10821106 }
10831107 }()
1084- ui . Render ( grid )
1108+ render ( )
10851109 done := make (chan struct {})
10861110 quit := make (chan os.Signal , 1 )
10871111 signal .Notify (quit , os .Interrupt , syscall .SIGTERM )
@@ -1091,63 +1115,84 @@ func main() {
10911115 }
10921116 }()
10931117 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 :
11021183 ui .Close ()
11031184 os .Exit (0 )
11041185 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- }
11441186 }
1145- case <- done :
1146- ui .Close ()
1147- os .Exit (0 )
1148- return
11491187 }
11501188 }
1189+
1190+ }
1191+
1192+ func render () {
1193+ if ! headless {
1194+ ui .Render (grid )
1195+ }
11511196}
11521197
11531198func setupLogfile () (* os.File , error ) {
@@ -1360,10 +1405,14 @@ func updateTotalPowerChart(watts float64) {
13601405 if count > 0 {
13611406 avgWatts = sum / float64 (count )
13621407 }
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+
13671416}
13681417
13691418func updateCPUUI (cpuMetrics CPUMetrics ) {
@@ -1372,38 +1421,48 @@ func updateCPUUI(cpuMetrics CPUMetrics) {
13721421 stderrLogger .Printf ("Error getting CPU percentages: %v\n " , err )
13731422 return
13741423 }
1375- cpuCoreWidget .UpdateUsage (coreUsages )
1424+ if ! headless {
1425+ cpuCoreWidget .UpdateUsage (coreUsages )
1426+ }
1427+
13761428 var totalUsage float64
13771429 for _ , usage := range coreUsages {
13781430 totalUsage += usage
13791431 }
13801432 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+
14041460 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+ }
14071466
14081467 cpuUsage .Set (float64 (totalUsage ))
14091468 powerUsage .With (prometheus.Labels {"component" : "cpu" }).Set (cpuMetrics .CPUW )
@@ -1417,8 +1476,10 @@ func updateCPUUI(cpuMetrics CPUMetrics) {
14171476}
14181477
14191478func 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+ }
14221483
14231484 // Add GPU history tracking
14241485 for i := 0 ; i < len (gpuValues )- 1 ; i ++ {
@@ -1440,9 +1501,11 @@ func updateGPUUI(gpuMetrics GPUMetrics) {
14401501 avgGPU = sum / float64 (count )
14411502 }
14421503
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+ }
14461509
14471510 gpuUsage .Set (float64 (gpuMetrics .Active ))
14481511 gpuFreqMHz .Set (float64 (gpuMetrics .FreqMHz ))
0 commit comments