Skip to content

Commit 4632e2f

Browse files
committed
add option to run program without UI
this allows for running prometheus in the background on mac startup
1 parent 347e6d9 commit 4632e2f

File tree

2 files changed

+166
-101
lines changed

2 files changed

+166
-101
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
mactop
44
dist
5-
.DS_Store
5+
.DS_Store
6+
.idea
7+
.vscode

main.go

+163-100
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ var (
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+
929937
func 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\nYou must use sudo to run mactop, as powermetrics requires root privileges.\n\nFor 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\nYou must use sudo to run mactop, as powermetrics requires root privileges.\n\nFor 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

11531198
func 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

13691418
func 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\nGPU Power: %.2f W\nTotal Power: %.2f W\nThermals: %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\nGPU Power: %.2f W\nTotal Power: %.2f W\nThermals: %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

14191478
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+
}
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

Comments
 (0)