Skip to content

Commit b2f82af

Browse files
committed
Add --color or -c option for customizable UI colors
1 parent 3509dee commit b2f82af

File tree

3 files changed

+73
-33
lines changed

3 files changed

+73
-33
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
- Disk Activity Read/Write
2222
- Easy-to-read terminal UI
2323
- Two layouts: default and alternative
24+
- Customizable UI color (green, red, blue, cyan, magenta, yellow, and white)
25+
- Customizable update interval (default is 1000ms)
2426
- Support for all Apple Silicon models.
2527

2628
## Install via Homebrew
@@ -83,6 +85,7 @@ sudo ./mactop
8385
## mactop Flags
8486

8587
- `--interval` or `-i`: Set the powermetrics update interval in milliseconds. Default is 1000. (For low-end M chips, you may want to increase this value)
88+
- `--color` or `-c`: Set the UI color. Default is white. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'. (-c green)
8689
- `--version` or `-v`: Print the version of mactop.
8790
- `--help` or `-h`: Show a help message about these flags and how to run mactop.
8891

@@ -92,6 +95,10 @@ Use the following keys to interact with the application while its running:
9295
- `r`: Refresh the UI data manually.
9396
- `l`: Toggle the current layout.
9497

98+
## Example Theme (Green) Screenshot (sudo mactop -c green)
99+
100+
![mactop theme](screenshot3.png)
101+
95102
## Confirmed tested working M series chips
96103

97104
- M1

main.go

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
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

49
package main
510

611
import (
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

7986
var (
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

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

447484
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
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\nGPU Power: %.1f W\nANE Power: %.1f W\nTotal 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

screenshot3.png

182 KB
Loading

0 commit comments

Comments
 (0)