mirror of
https://github.com/tw93/Mole.git
synced 2026-02-11 11:14:16 +00:00
System Status Dashboard
This commit is contained in:
23
.github/workflows/shell-quality-checks.yml
vendored
23
.github/workflows/shell-quality-checks.yml
vendored
@@ -57,15 +57,24 @@ jobs:
|
|||||||
- name: Build Universal Binary for disk analyzer
|
- name: Build Universal Binary for disk analyzer
|
||||||
run: ./scripts/build-analyze.sh
|
run: ./scripts/build-analyze.sh
|
||||||
|
|
||||||
|
- name: Build Universal Binary for system status
|
||||||
|
run: ./scripts/build-status.sh
|
||||||
|
|
||||||
- name: Verify binary is valid
|
- name: Verify binary is valid
|
||||||
run: |
|
run: |
|
||||||
if [[ ! -x bin/analyze-go ]]; then
|
if [[ ! -x bin/analyze-go ]]; then
|
||||||
echo "Error: bin/analyze-go is not executable"
|
echo "Error: bin/analyze-go is not executable"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [[ ! -x bin/status-go ]]; then
|
||||||
|
echo "Error: bin/status-go is not executable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "Binary info:"
|
echo "Binary info:"
|
||||||
file bin/analyze-go
|
file bin/analyze-go
|
||||||
ls -lh bin/analyze-go
|
ls -lh bin/analyze-go
|
||||||
|
file bin/status-go
|
||||||
|
ls -lh bin/status-go
|
||||||
echo ""
|
echo ""
|
||||||
echo "✓ Universal binary built successfully"
|
echo "✓ Universal binary built successfully"
|
||||||
|
|
||||||
@@ -74,11 +83,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if git diff --quiet -- bin/analyze-go; then
|
if git diff --quiet -- bin/analyze-go; then
|
||||||
echo "bin/analyze-go unchanged; nothing to commit."
|
echo "bin/analyze-go unchanged; nothing to commit."
|
||||||
|
else
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add bin/analyze-go
|
||||||
|
git commit -m "chore: update analyzer binary"
|
||||||
|
git push origin HEAD:${GITHUB_REF#refs/heads/}
|
||||||
|
fi
|
||||||
|
if git diff --quiet -- bin/status-go; then
|
||||||
|
echo "bin/status-go unchanged; nothing to commit."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git add bin/analyze-go
|
git add bin/status-go
|
||||||
git commit -m "chore: update analyzer binary"
|
git commit -m "chore: update status binary"
|
||||||
git push origin HEAD:${GITHUB_REF#refs/heads/}
|
git push origin HEAD:${GITHUB_REF#refs/heads/}
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -44,3 +44,5 @@ copilot-instructions.md
|
|||||||
|
|
||||||
# Go build artifacts
|
# Go build artifacts
|
||||||
cmd/analyze/analyze
|
cmd/analyze/analyze
|
||||||
|
cmd/status/status
|
||||||
|
/status
|
||||||
|
|||||||
41
README.md
41
README.md
@@ -23,6 +23,7 @@
|
|||||||
- **Thorough Uninstall** - Scans 22+ locations to remove app leftovers, not just the .app file
|
- **Thorough Uninstall** - Scans 22+ locations to remove app leftovers, not just the .app file
|
||||||
- **System Optimization** - Rebuilds caches, resets services, and trims swap/network cruft with one run
|
- **System Optimization** - Rebuilds caches, resets services, and trims swap/network cruft with one run
|
||||||
- **Interactive Disk Analyzer** - Navigate folders with arrow keys, find and delete large files quickly
|
- **Interactive Disk Analyzer** - Navigate folders with arrow keys, find and delete large files quickly
|
||||||
|
- **System Status Dashboard** - Real-time health score with live CPU/GPU/Memory/Disk/Network/Battery metrics
|
||||||
- **Fast & Lightweight** - Terminal-based with arrow-key navigation, pagination, and Touch ID support
|
- **Fast & Lightweight** - Terminal-based with arrow-key navigation, pagination, and Touch ID support
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -49,6 +50,7 @@ mo clean --whitelist # Manage protected caches
|
|||||||
mo uninstall # Uninstall apps
|
mo uninstall # Uninstall apps
|
||||||
mo optimize # System optimization
|
mo optimize # System optimization
|
||||||
mo analyze # Disk analyzer
|
mo analyze # Disk analyzer
|
||||||
|
mo status # Live system status dashboard
|
||||||
|
|
||||||
mo touchid # Configure Touch ID for sudo
|
mo touchid # Configure Touch ID for sudo
|
||||||
mo update # Update Mole
|
mo update # Update Mole
|
||||||
@@ -145,6 +147,35 @@ Analyze Disk ~/Documents | Total: 156.8GB
|
|||||||
↑↓←→ Navigate | O Open | F Reveal | ⌫ Delete | L Large(24) | Q Quit
|
↑↓←→ Navigate | O Open | F Reveal | ⌫ Delete | L Large(24) | Q Quit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Live System Status
|
||||||
|
|
||||||
|
Real-time dashboard with system health score, hardware info, and performance metrics.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mo status
|
||||||
|
|
||||||
|
Mole Status Health ● 92 MacBook Pro · Apple M4 Pro · 32.0 GB · 460.4 GB · macOS 14.5
|
||||||
|
|
||||||
|
⚙ CPU ────────────────────── ▦ Memory ─────────────────────
|
||||||
|
Total ████████████░░░░░░ 45.2% Used ███████████░░░░░░ 58.4%
|
||||||
|
0.82 / 1.05 / 1.23 (8 cores) 14.2 GB / 24.0 GB total
|
||||||
|
Core1 ███████████████░░░ 78.3% Free ████████░░░░░░░░░ 41.6%
|
||||||
|
Core2 ████████████░░░░░░ 62.1% 9.8 GB available
|
||||||
|
|
||||||
|
▤ Disk ────────────────────── ▮ Power ──────────────────────
|
||||||
|
Used █████████████░░░░░ 67.2% 100% ██████████████████ 100%
|
||||||
|
156.3 GB free Charged ⚡
|
||||||
|
Read ▮▯▯▯▯ 2.1 MB/s Normal · 423 cycles
|
||||||
|
Write ▮▮▮▯▯ 18.3 MB/s 58°C · 1200 RPM
|
||||||
|
|
||||||
|
⇅ Network ─────────────────── ▶ Processes ───────────────────
|
||||||
|
Down ▮▮▯▯▯ 3.2 MB/s Code ▮▮▮▮▯ 42.1%
|
||||||
|
Up ▮▯▯▯▯ 0.8 MB/s Chrome ▮▮▮▯▯ 28.3%
|
||||||
|
Proxy: HTTP · 192.168.1.100 Terminal ▮▯▯▯▯ 12.5%
|
||||||
|
```
|
||||||
|
|
||||||
|
Health score is calculated from CPU usage, memory pressure, disk space, temperature, and I/O load. Color-coded: 90-100 green, 75-89 light green, 60-74 yellow, 40-59 orange, 0-39 red.
|
||||||
|
|
||||||
## Quick Launchers
|
## Quick Launchers
|
||||||
|
|
||||||
Launch Mole commands instantly from Raycast or Alfred:
|
Launch Mole commands instantly from Raycast or Alfred:
|
||||||
@@ -153,7 +184,15 @@ Launch Mole commands instantly from Raycast or Alfred:
|
|||||||
curl -fsSL https://raw.githubusercontent.com/tw93/Mole/main/scripts/setup-quick-launchers.sh | bash
|
curl -fsSL https://raw.githubusercontent.com/tw93/Mole/main/scripts/setup-quick-launchers.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Adds 4 commands: `clean`, `uninstall`, `optimize`, `analyze`. Auto-detects your terminal or set `MO_LAUNCHER_APP=<name>` to override.
|
Adds 5 commands: `clean`, `uninstall`, `optimize`, `analyze`, `status`. Auto-detects your terminal or set `MO_LAUNCHER_APP=<name>` to override.
|
||||||
|
|
||||||
|
**Reload Raycast scripts after installation:**
|
||||||
|
|
||||||
|
1. Open Raycast (⌘ Space)
|
||||||
|
2. Search for "Reload Script Directories"
|
||||||
|
3. Press Enter to activate new commands
|
||||||
|
|
||||||
|
Alternatively, restart Raycast to load the new scripts.
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
|
|||||||
BIN
bin/status-go
Executable file
BIN
bin/status-go
Executable file
Binary file not shown.
13
bin/status.sh
Executable file
13
bin/status.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Entry point for the Go-based system status panel bundled with Mole.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
GO_BIN="$SCRIPT_DIR/status-go"
|
||||||
|
if [[ -x "$GO_BIN" ]]; then
|
||||||
|
exec "$GO_BIN" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Bundled status binary not found. Please reinstall Mole or run mo update to restore it." >&2
|
||||||
|
exit 1
|
||||||
140
cmd/status/main.go
Normal file
140
cmd/status/main.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshInterval = time.Second
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
BuildTime = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type tickMsg struct{}
|
||||||
|
type animTickMsg struct{}
|
||||||
|
|
||||||
|
type metricsMsg struct {
|
||||||
|
data MetricsSnapshot
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
collector *Collector
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
metrics MetricsSnapshot
|
||||||
|
errMessage string
|
||||||
|
ready bool
|
||||||
|
lastUpdated time.Time
|
||||||
|
collecting bool
|
||||||
|
animFrame int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newModel() model {
|
||||||
|
return model{
|
||||||
|
collector: NewCollector(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return tea.Batch(tickAfter(0), animTick())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "q", "esc", "ctrl+c":
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
return m, nil
|
||||||
|
case tickMsg:
|
||||||
|
if m.collecting {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.collecting = true
|
||||||
|
return m, m.collectCmd()
|
||||||
|
case metricsMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.errMessage = msg.err.Error()
|
||||||
|
} else {
|
||||||
|
m.errMessage = ""
|
||||||
|
}
|
||||||
|
m.metrics = msg.data
|
||||||
|
m.lastUpdated = msg.data.CollectedAt
|
||||||
|
m.collecting = false
|
||||||
|
// Mark ready after first successful data collection
|
||||||
|
if !m.ready {
|
||||||
|
m.ready = true
|
||||||
|
}
|
||||||
|
return m, tickAfter(refreshInterval)
|
||||||
|
case animTickMsg:
|
||||||
|
m.animFrame++
|
||||||
|
return m, animTickWithSpeed(m.metrics.CPU.Usage)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
if !m.ready {
|
||||||
|
return "Loading..."
|
||||||
|
}
|
||||||
|
|
||||||
|
header := renderHeader(m.metrics, m.errMessage, m.animFrame, m.width)
|
||||||
|
cardWidth := 0
|
||||||
|
if m.width > 80 {
|
||||||
|
cardWidth = maxInt(24, m.width/2-4)
|
||||||
|
}
|
||||||
|
cards := buildCards(m.metrics, cardWidth)
|
||||||
|
|
||||||
|
if m.width <= 80 {
|
||||||
|
var rendered []string
|
||||||
|
for _, c := range cards {
|
||||||
|
rendered = append(rendered, renderCard(c, cardWidth, 0))
|
||||||
|
}
|
||||||
|
return header + "\n" + lipgloss.JoinVertical(lipgloss.Left, rendered...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return header + "\n" + renderTwoColumns(cards, m.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) collectCmd() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
data, err := m.collector.Collect()
|
||||||
|
return metricsMsg{data: data, err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tickAfter(delay time.Duration) tea.Cmd {
|
||||||
|
return tea.Tick(delay, func(time.Time) tea.Msg { return tickMsg{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
func animTick() tea.Cmd {
|
||||||
|
return tea.Tick(200*time.Millisecond, func(time.Time) tea.Msg { return animTickMsg{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
func animTickWithSpeed(cpuUsage float64) tea.Cmd {
|
||||||
|
// Higher CPU = faster animation (50ms to 300ms)
|
||||||
|
interval := 300 - int(cpuUsage*2.5)
|
||||||
|
if interval < 50 {
|
||||||
|
interval = 50
|
||||||
|
}
|
||||||
|
return tea.Tick(time.Duration(interval)*time.Millisecond, func(time.Time) tea.Msg { return animTickMsg{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
p := tea.NewProgram(newModel(), tea.WithAltScreen())
|
||||||
|
if err := p.Start(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "system status error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
1244
cmd/status/metrics.go
Normal file
1244
cmd/status/metrics.go
Normal file
File diff suppressed because it is too large
Load Diff
594
cmd/status/view.go
Normal file
594
cmd/status/view.go
Normal file
@@ -0,0 +1,594 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#5FD7FF")).Bold(true)
|
||||||
|
subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6C6C6C"))
|
||||||
|
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD75F"))
|
||||||
|
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F5F")).Bold(true)
|
||||||
|
okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#87D787"))
|
||||||
|
lineStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#4A4A4A"))
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
colWidth = 38
|
||||||
|
iconCPU = "⚙"
|
||||||
|
iconMemory = "▦"
|
||||||
|
iconGPU = "▣"
|
||||||
|
iconDisk = "▤"
|
||||||
|
iconNetwork = "⇅"
|
||||||
|
iconBattery = "▮"
|
||||||
|
iconSensors = "♨"
|
||||||
|
iconProcs = "▶"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mole body frames (legs animate)
|
||||||
|
var moleBody = [][]string{
|
||||||
|
{
|
||||||
|
` /\_/\`,
|
||||||
|
` ___/ o o \`,
|
||||||
|
`/___ =-= /`,
|
||||||
|
`\____)-m-m)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
` /\_/\`,
|
||||||
|
` ___/ o o \`,
|
||||||
|
`/___ =-= /`,
|
||||||
|
`\____)mm__)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
` /\_/\`,
|
||||||
|
` ___/ · · \`,
|
||||||
|
`/___ =-= /`,
|
||||||
|
`\___)-m__m)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
` /\_/\`,
|
||||||
|
` ___/ o o \`,
|
||||||
|
`/___ =-= /`,
|
||||||
|
`\____)-mm-)`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate frames with horizontal movement
|
||||||
|
func getMoleFrame(animFrame int, termWidth int) string {
|
||||||
|
bodyIdx := animFrame % len(moleBody)
|
||||||
|
body := moleBody[bodyIdx]
|
||||||
|
|
||||||
|
// Calculate mole width (approximate)
|
||||||
|
moleWidth := 15
|
||||||
|
// Move across terminal width
|
||||||
|
maxPos := termWidth - moleWidth
|
||||||
|
if maxPos < 0 {
|
||||||
|
maxPos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move position: 0 -> maxPos -> 0
|
||||||
|
cycleLength := maxPos * 2
|
||||||
|
if cycleLength == 0 {
|
||||||
|
cycleLength = 1
|
||||||
|
}
|
||||||
|
pos := animFrame % cycleLength
|
||||||
|
if pos > maxPos {
|
||||||
|
pos = cycleLength - pos
|
||||||
|
}
|
||||||
|
|
||||||
|
padding := strings.Repeat(" ", pos)
|
||||||
|
var lines []string
|
||||||
|
for _, line := range body {
|
||||||
|
lines = append(lines, padding+line)
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
type cardData struct {
|
||||||
|
icon string
|
||||||
|
title string
|
||||||
|
lines []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderHeader(m MetricsSnapshot, errMsg string, animFrame int, termWidth int) string {
|
||||||
|
// Title
|
||||||
|
title := titleStyle.Render("Mole Status")
|
||||||
|
|
||||||
|
// Health Score with color and label
|
||||||
|
scoreStyle := getScoreStyle(m.HealthScore)
|
||||||
|
scoreText := subtleStyle.Render("Health ") + scoreStyle.Render(fmt.Sprintf("● %d", m.HealthScore))
|
||||||
|
|
||||||
|
// Hardware info
|
||||||
|
infoParts := []string{}
|
||||||
|
if m.Hardware.Model != "" {
|
||||||
|
infoParts = append(infoParts, m.Hardware.Model)
|
||||||
|
}
|
||||||
|
if m.Hardware.CPUModel != "" {
|
||||||
|
infoParts = append(infoParts, m.Hardware.CPUModel)
|
||||||
|
}
|
||||||
|
if m.Hardware.TotalRAM != "" {
|
||||||
|
infoParts = append(infoParts, m.Hardware.TotalRAM)
|
||||||
|
}
|
||||||
|
if m.Hardware.DiskSize != "" {
|
||||||
|
infoParts = append(infoParts, m.Hardware.DiskSize)
|
||||||
|
}
|
||||||
|
if m.Hardware.OSVersion != "" {
|
||||||
|
infoParts = append(infoParts, m.Hardware.OSVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
headerLine := title + " " + scoreText + " " + subtleStyle.Render(strings.Join(infoParts, " · "))
|
||||||
|
|
||||||
|
// Running mole animation
|
||||||
|
mole := getMoleFrame(animFrame, termWidth)
|
||||||
|
|
||||||
|
if errMsg != "" {
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, headerLine, "", mole, dangerStyle.Render(errMsg), "")
|
||||||
|
}
|
||||||
|
return headerLine + "\n\n" + mole
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScoreStyle(score int) lipgloss.Style {
|
||||||
|
if score >= 90 {
|
||||||
|
// Excellent - Green
|
||||||
|
return lipgloss.NewStyle().Foreground(lipgloss.Color("#87D787")).Bold(true)
|
||||||
|
} else if score >= 75 {
|
||||||
|
// Good - Light Green
|
||||||
|
return lipgloss.NewStyle().Foreground(lipgloss.Color("#AFD787")).Bold(true)
|
||||||
|
} else if score >= 60 {
|
||||||
|
// Fair - Yellow
|
||||||
|
return lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD75F")).Bold(true)
|
||||||
|
} else if score >= 40 {
|
||||||
|
// Poor - Orange
|
||||||
|
return lipgloss.NewStyle().Foreground(lipgloss.Color("#FFAF5F")).Bold(true)
|
||||||
|
} else {
|
||||||
|
// Critical - Red
|
||||||
|
return lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F5F")).Bold(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCards(m MetricsSnapshot, _ int) []cardData {
|
||||||
|
// Row 1: CPU + Memory
|
||||||
|
// Row 2: Disk + Power
|
||||||
|
// Row 3: Top Processes + Network
|
||||||
|
cards := []cardData{
|
||||||
|
renderCPUCard(m.CPU),
|
||||||
|
renderMemoryCard(m.Memory),
|
||||||
|
renderDiskCard(m.Disks, m.DiskIO),
|
||||||
|
renderBatteryCard(m.Batteries, m.Thermal),
|
||||||
|
renderProcessCard(m.TopProcesses),
|
||||||
|
renderNetworkCard(m.Network, m.Proxy),
|
||||||
|
}
|
||||||
|
// Only show GPU card if there are GPUs with usage data
|
||||||
|
if len(m.GPU) > 0 && m.GPU[0].Usage >= 0 {
|
||||||
|
cards = append(cards, renderGPUCard(m.GPU))
|
||||||
|
}
|
||||||
|
// Only show sensors if we have valid temperature readings
|
||||||
|
if hasSensorData(m.Sensors) {
|
||||||
|
cards = append(cards, renderSensorsCard(m.Sensors))
|
||||||
|
}
|
||||||
|
return cards
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasSensorData(sensors []SensorReading) bool {
|
||||||
|
for _, s := range sensors {
|
||||||
|
if s.Note == "" && s.Value > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderCPUCard(cpu CPUStatus) cardData {
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, fmt.Sprintf("Total %s %5.1f%%", progressBar(cpu.Usage), cpu.Usage))
|
||||||
|
lines = append(lines, subtleStyle.Render(fmt.Sprintf("%.2f / %.2f / %.2f (%d cores)", cpu.Load1, cpu.Load5, cpu.Load15, cpu.LogicalCPU)))
|
||||||
|
|
||||||
|
// Show top 3 busiest cores
|
||||||
|
type coreUsage struct {
|
||||||
|
idx int
|
||||||
|
val float64
|
||||||
|
}
|
||||||
|
var cores []coreUsage
|
||||||
|
for i, v := range cpu.PerCore {
|
||||||
|
cores = append(cores, coreUsage{i, v})
|
||||||
|
}
|
||||||
|
sort.Slice(cores, func(i, j int) bool { return cores[i].val > cores[j].val })
|
||||||
|
|
||||||
|
maxCores := 3
|
||||||
|
if len(cores) < maxCores {
|
||||||
|
maxCores = len(cores)
|
||||||
|
}
|
||||||
|
for i := 0; i < maxCores; i++ {
|
||||||
|
c := cores[i]
|
||||||
|
lines = append(lines, fmt.Sprintf("Core%-2d %s %5.1f%%", c.idx+1, progressBar(c.val), c.val))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cardData{icon: iconCPU, title: "CPU", lines: lines}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderGPUCard(gpus []GPUStatus) cardData {
|
||||||
|
var lines []string
|
||||||
|
if len(gpus) == 0 {
|
||||||
|
lines = append(lines, subtleStyle.Render("No GPU detected"))
|
||||||
|
} else {
|
||||||
|
for _, g := range gpus {
|
||||||
|
name := shorten(g.Name, 12)
|
||||||
|
if g.Usage >= 0 {
|
||||||
|
lines = append(lines, fmt.Sprintf("%-12s %s %5.1f%%", name, progressBar(g.Usage), g.Usage))
|
||||||
|
} else {
|
||||||
|
lines = append(lines, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cardData{icon: iconGPU, title: "GPU", lines: lines}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderMemoryCard(mem MemoryStatus) cardData {
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, fmt.Sprintf("Used %s %5.1f%%", progressBar(mem.UsedPercent), mem.UsedPercent))
|
||||||
|
lines = append(lines, subtleStyle.Render(fmt.Sprintf("%s / %s total", humanBytes(mem.Used), humanBytes(mem.Total))))
|
||||||
|
lines = append(lines, "")
|
||||||
|
// Show available memory
|
||||||
|
available := mem.Total - mem.Used
|
||||||
|
freePercent := 100 - mem.UsedPercent
|
||||||
|
lines = append(lines, fmt.Sprintf("Free %s %5.1f%%", progressBar(freePercent), freePercent))
|
||||||
|
lines = append(lines, subtleStyle.Render(fmt.Sprintf("%s available", humanBytes(available))))
|
||||||
|
// Memory pressure
|
||||||
|
if mem.Pressure != "" {
|
||||||
|
pressureStyle := okStyle
|
||||||
|
pressureText := "Status " + mem.Pressure
|
||||||
|
if mem.Pressure == "warn" {
|
||||||
|
pressureStyle = warnStyle
|
||||||
|
} else if mem.Pressure == "critical" {
|
||||||
|
pressureStyle = dangerStyle
|
||||||
|
}
|
||||||
|
lines = append(lines, pressureStyle.Render(pressureText))
|
||||||
|
}
|
||||||
|
return cardData{icon: iconMemory, title: "Memory", lines: lines}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderDiskCard(disks []DiskStatus, io DiskIOStatus) cardData {
|
||||||
|
var lines []string
|
||||||
|
// Show main disk
|
||||||
|
if len(disks) > 0 {
|
||||||
|
d := disks[0]
|
||||||
|
freeSpace := d.Total - d.Used
|
||||||
|
bar := diskBar(d.UsedPercent)
|
||||||
|
lines = append(lines, fmt.Sprintf("Used %s %4.0f%% (%s free)", bar, d.UsedPercent, humanBytes(freeSpace)))
|
||||||
|
}
|
||||||
|
// IO
|
||||||
|
readBar := ioBar(io.ReadRate)
|
||||||
|
writeBar := ioBar(io.WriteRate)
|
||||||
|
lines = append(lines, fmt.Sprintf("Read %s %.1f MB/s", readBar, io.ReadRate))
|
||||||
|
lines = append(lines, fmt.Sprintf("Write %s %.1f MB/s", writeBar, io.WriteRate))
|
||||||
|
return cardData{icon: iconDisk, title: "Disk", lines: lines}
|
||||||
|
}
|
||||||
|
|
||||||
|
func diskBar(percent float64) string {
|
||||||
|
total := 16
|
||||||
|
filled := int(percent / 100 * float64(total))
|
||||||
|
if filled > total {
|
||||||
|
filled = total
|
||||||
|
}
|
||||||
|
bar := strings.Repeat("█", filled) + strings.Repeat("░", total-filled)
|
||||||
|
return colorizePercent(percent, bar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ioBar(rate float64) string {
|
||||||
|
// Scale: 0-50 MB/s maps to 0-5 blocks
|
||||||
|
filled := int(rate / 10.0)
|
||||||
|
if filled > 5 {
|
||||||
|
filled = 5
|
||||||
|
}
|
||||||
|
if filled < 0 {
|
||||||
|
filled = 0
|
||||||
|
}
|
||||||
|
bar := strings.Repeat("▮", filled) + strings.Repeat("▯", 5-filled)
|
||||||
|
if rate > 80 {
|
||||||
|
return dangerStyle.Render(bar)
|
||||||
|
}
|
||||||
|
if rate > 30 {
|
||||||
|
return warnStyle.Render(bar)
|
||||||
|
}
|
||||||
|
return okStyle.Render(bar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderProcessCard(procs []ProcessInfo) cardData {
|
||||||
|
var lines []string
|
||||||
|
maxProcs := 3
|
||||||
|
for i, p := range procs {
|
||||||
|
if i >= maxProcs {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
name := shorten(p.Name, 12)
|
||||||
|
cpuBar := miniBar(p.CPU)
|
||||||
|
lines = append(lines, fmt.Sprintf("%-12s %s %5.1f%%", name, cpuBar, p.CPU))
|
||||||
|
}
|
||||||
|
if len(lines) == 0 {
|
||||||
|
lines = append(lines, subtleStyle.Render("No data"))
|
||||||
|
}
|
||||||
|
return cardData{icon: iconProcs, title: "Processes", lines: lines}
|
||||||
|
}
|
||||||
|
|
||||||
|
func miniBar(percent float64) string {
|
||||||
|
filled := int(percent / 20) // 5 chars max for 100%
|
||||||
|
if filled > 5 {
|
||||||
|
filled = 5
|
||||||
|
}
|
||||||
|
if filled < 0 {
|
||||||
|
filled = 0
|
||||||
|
}
|
||||||
|
return colorizePercent(percent, strings.Repeat("▮", filled)+strings.Repeat("▯", 5-filled))
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderNetworkCard(netStats []NetworkStatus, proxy ProxyStatus) cardData {
|
||||||
|
var lines []string
|
||||||
|
var totalRx, totalTx float64
|
||||||
|
var primaryIP string
|
||||||
|
|
||||||
|
for _, n := range netStats {
|
||||||
|
totalRx += n.RxRateMBs
|
||||||
|
totalTx += n.TxRateMBs
|
||||||
|
if primaryIP == "" && n.IP != "" && n.Name == "en0" {
|
||||||
|
primaryIP = n.IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(netStats) == 0 {
|
||||||
|
lines = []string{subtleStyle.Render("Collecting...")}
|
||||||
|
} else {
|
||||||
|
rxBar := netBar(totalRx)
|
||||||
|
txBar := netBar(totalTx)
|
||||||
|
lines = append(lines, fmt.Sprintf("Down %s %s", rxBar, formatRate(totalRx)))
|
||||||
|
lines = append(lines, fmt.Sprintf("Up %s %s", txBar, formatRate(totalTx)))
|
||||||
|
// Proxy + IP
|
||||||
|
info := ""
|
||||||
|
if proxy.Enabled {
|
||||||
|
info = okStyle.Render("Proxy: " + proxy.Type)
|
||||||
|
}
|
||||||
|
if primaryIP != "" {
|
||||||
|
if info != "" {
|
||||||
|
info += " · "
|
||||||
|
}
|
||||||
|
info += primaryIP
|
||||||
|
}
|
||||||
|
if info != "" {
|
||||||
|
lines = append(lines, subtleStyle.Render(info))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cardData{icon: iconNetwork, title: "Network", lines: lines}
|
||||||
|
}
|
||||||
|
|
||||||
|
func netBar(rate float64) string {
|
||||||
|
// Scale: 0-10 MB/s maps to 0-5 blocks
|
||||||
|
filled := int(rate / 2.0)
|
||||||
|
if filled > 5 {
|
||||||
|
filled = 5
|
||||||
|
}
|
||||||
|
if filled < 0 {
|
||||||
|
filled = 0
|
||||||
|
}
|
||||||
|
bar := strings.Repeat("▮", filled) + strings.Repeat("▯", 5-filled)
|
||||||
|
if rate > 8 {
|
||||||
|
return dangerStyle.Render(bar)
|
||||||
|
}
|
||||||
|
if rate > 3 {
|
||||||
|
return warnStyle.Render(bar)
|
||||||
|
}
|
||||||
|
return okStyle.Render(bar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData {
|
||||||
|
var lines []string
|
||||||
|
if len(batts) == 0 {
|
||||||
|
lines = append(lines, subtleStyle.Render("No battery"))
|
||||||
|
} else {
|
||||||
|
b := batts[0]
|
||||||
|
// Line 1: label + percentage + bar
|
||||||
|
lines = append(lines, fmt.Sprintf("Level %3.0f%% %s", b.Percent, progressBar(b.Percent)))
|
||||||
|
|
||||||
|
// Line 2: status
|
||||||
|
statusIcon := ""
|
||||||
|
statusStyle := subtleStyle
|
||||||
|
statusLower := strings.ToLower(b.Status)
|
||||||
|
if statusLower == "charging" || statusLower == "charged" {
|
||||||
|
statusIcon = " ⚡"
|
||||||
|
statusStyle = okStyle
|
||||||
|
} else if b.Percent < 20 {
|
||||||
|
statusStyle = dangerStyle
|
||||||
|
}
|
||||||
|
// Capitalize first letter
|
||||||
|
statusText := b.Status
|
||||||
|
if len(statusText) > 0 {
|
||||||
|
statusText = strings.ToUpper(statusText[:1]) + strings.ToLower(statusText[1:])
|
||||||
|
}
|
||||||
|
if b.TimeLeft != "" {
|
||||||
|
statusText += " · " + b.TimeLeft
|
||||||
|
}
|
||||||
|
lines = append(lines, statusStyle.Render(statusText+statusIcon))
|
||||||
|
|
||||||
|
// Line 3: Health + cycles
|
||||||
|
healthParts := []string{}
|
||||||
|
if b.Health != "" {
|
||||||
|
healthParts = append(healthParts, b.Health)
|
||||||
|
}
|
||||||
|
if b.CycleCount > 0 {
|
||||||
|
healthParts = append(healthParts, fmt.Sprintf("%d cycles", b.CycleCount))
|
||||||
|
}
|
||||||
|
if len(healthParts) > 0 {
|
||||||
|
lines = append(lines, subtleStyle.Render(strings.Join(healthParts, " · ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line 4: Temp + Fan combined
|
||||||
|
var thermalParts []string
|
||||||
|
if thermal.CPUTemp > 0 {
|
||||||
|
tempStyle := okStyle
|
||||||
|
if thermal.CPUTemp > 80 {
|
||||||
|
tempStyle = dangerStyle
|
||||||
|
} else if thermal.CPUTemp > 60 {
|
||||||
|
tempStyle = warnStyle
|
||||||
|
}
|
||||||
|
thermalParts = append(thermalParts, tempStyle.Render(fmt.Sprintf("%.0f°C", thermal.CPUTemp)))
|
||||||
|
}
|
||||||
|
if thermal.FanSpeed > 0 {
|
||||||
|
thermalParts = append(thermalParts, fmt.Sprintf("%d RPM", thermal.FanSpeed))
|
||||||
|
}
|
||||||
|
if len(thermalParts) > 0 {
|
||||||
|
lines = append(lines, strings.Join(thermalParts, " · "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cardData{icon: iconBattery, title: "Power", lines: lines}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderSensorsCard(sensors []SensorReading) cardData {
|
||||||
|
var lines []string
|
||||||
|
for _, s := range sensors {
|
||||||
|
if s.Note != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("%-12s %s", shorten(s.Label, 12), colorizeTemp(s.Value)+s.Unit))
|
||||||
|
}
|
||||||
|
if len(lines) == 0 {
|
||||||
|
lines = append(lines, subtleStyle.Render("No sensors"))
|
||||||
|
}
|
||||||
|
return cardData{icon: iconSensors, title: "Sensors", lines: lines}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func renderCard(data cardData, width int, height int) string {
|
||||||
|
titleText := data.icon + " " + data.title
|
||||||
|
lineLen := width - lipgloss.Width(titleText) - 1
|
||||||
|
if lineLen < 4 {
|
||||||
|
lineLen = 4
|
||||||
|
}
|
||||||
|
header := titleStyle.Render(titleText) + " " + lineStyle.Render(strings.Repeat("─", lineLen))
|
||||||
|
content := header + "\n" + strings.Join(data.lines, "\n") + "\n"
|
||||||
|
|
||||||
|
// Pad to target height
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for len(lines) < height {
|
||||||
|
lines = append(lines, "")
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func progressBar(percent float64) string {
|
||||||
|
total := 18
|
||||||
|
if percent < 0 {
|
||||||
|
percent = 0
|
||||||
|
}
|
||||||
|
if percent > 100 {
|
||||||
|
percent = 100
|
||||||
|
}
|
||||||
|
filled := int(percent / 100 * float64(total))
|
||||||
|
if filled > total {
|
||||||
|
filled = total
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
if i < filled {
|
||||||
|
builder.WriteString("█")
|
||||||
|
} else {
|
||||||
|
builder.WriteString("░")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colorizePercent(percent, builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorizePercent(percent float64, s string) string {
|
||||||
|
switch {
|
||||||
|
case percent >= 90:
|
||||||
|
return dangerStyle.Render(s)
|
||||||
|
case percent >= 70:
|
||||||
|
return warnStyle.Render(s)
|
||||||
|
default:
|
||||||
|
return okStyle.Render(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorizeTemp(t float64) string {
|
||||||
|
switch {
|
||||||
|
case t >= 85:
|
||||||
|
return dangerStyle.Render(fmt.Sprintf("%.1f", t))
|
||||||
|
case t >= 70:
|
||||||
|
return warnStyle.Render(fmt.Sprintf("%.1f", t))
|
||||||
|
default:
|
||||||
|
return subtleStyle.Render(fmt.Sprintf("%.1f", t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRate(mb float64) string {
|
||||||
|
if mb < 0.01 {
|
||||||
|
return "0 MB/s"
|
||||||
|
}
|
||||||
|
if mb < 1 {
|
||||||
|
return fmt.Sprintf("%.2f MB/s", mb)
|
||||||
|
}
|
||||||
|
if mb < 10 {
|
||||||
|
return fmt.Sprintf("%.1f MB/s", mb)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.0f MB/s", mb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanBytes(v uint64) string {
|
||||||
|
switch {
|
||||||
|
case v > 1<<40:
|
||||||
|
return fmt.Sprintf("%.1f TB", float64(v)/(1<<40))
|
||||||
|
case v > 1<<30:
|
||||||
|
return fmt.Sprintf("%.1f GB", float64(v)/(1<<30))
|
||||||
|
case v > 1<<20:
|
||||||
|
return fmt.Sprintf("%.1f MB", float64(v)/(1<<20))
|
||||||
|
case v > 1<<10:
|
||||||
|
return fmt.Sprintf("%.1f KB", float64(v)/(1<<10))
|
||||||
|
default:
|
||||||
|
return strconv.FormatUint(v, 10) + " B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shorten(s string, max int) string {
|
||||||
|
if len(s) <= max {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:max-1] + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTwoColumns(cards []cardData, width int) string {
|
||||||
|
if len(cards) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cw := colWidth
|
||||||
|
if width > 0 && width/2-2 > cw {
|
||||||
|
cw = width/2 - 2
|
||||||
|
}
|
||||||
|
var rows []string
|
||||||
|
for i := 0; i < len(cards); i += 2 {
|
||||||
|
left := renderCard(cards[i], cw, 0)
|
||||||
|
right := ""
|
||||||
|
if i+1 < len(cards) {
|
||||||
|
right = renderCard(cards[i+1], cw, 0)
|
||||||
|
}
|
||||||
|
targetHeight := maxInt(lipgloss.Height(left), lipgloss.Height(right))
|
||||||
|
left = renderCard(cards[i], cw, targetHeight)
|
||||||
|
if right != "" {
|
||||||
|
right = renderCard(cards[i+1], cw, targetHeight)
|
||||||
|
rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, left, " ", right))
|
||||||
|
} else {
|
||||||
|
rows = append(rows, left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxInt(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
10
go.mod
10
go.mod
@@ -7,26 +7,34 @@ toolchain go1.24.6
|
|||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.3.0
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5
|
||||||
golang.org/x/sync v0.18.0
|
golang.org/x/sync v0.18.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.7 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.3.8 // indirect
|
golang.org/x/text v0.3.8 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
34
go.sum
34
go.sum
@@ -14,10 +14,19 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G
|
|||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
@@ -30,18 +39,43 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
|||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
|
||||||
|
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
|
||||||
|
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
14
mole
14
mole
@@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
source "$SCRIPT_DIR/lib/common.sh"
|
source "$SCRIPT_DIR/lib/common.sh"
|
||||||
|
|
||||||
# Version info
|
# Version info
|
||||||
VERSION="1.9.20"
|
VERSION="1.10.0"
|
||||||
MOLE_TAGLINE="can dig deep to clean your Mac."
|
MOLE_TAGLINE="can dig deep to clean your Mac."
|
||||||
|
|
||||||
# Get latest version from remote repository
|
# Get latest version from remote repository
|
||||||
@@ -169,6 +169,7 @@ show_help() {
|
|||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo uninstall" "$NC" "Remove applications completely"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo uninstall" "$NC" "Remove applications completely"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize" "$NC" "System health check & optimization"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize" "$NC" "System health check & optimization"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo analyze" "$NC" "Interactive disk space explorer"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo analyze" "$NC" "Interactive disk space explorer"
|
||||||
|
printf " %s%-28s%s %s\n" "$GREEN" "mo status" "$NC" "System status dashboard"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo touchid" "$NC" "Configure Touch ID for sudo"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo touchid" "$NC" "Configure Touch ID for sudo"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo update" "$NC" "Update Mole to the latest version"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo update" "$NC" "Update Mole to the latest version"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo remove" "$NC" "Remove Mole from the system"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo remove" "$NC" "Remove Mole from the system"
|
||||||
@@ -503,6 +504,7 @@ show_main_menu() {
|
|||||||
printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")"
|
printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")"
|
||||||
printf '\r\033[2K%s\n' "$(show_menu_option 3 "Optimize Mac - System health & tuning" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
|
printf '\r\033[2K%s\n' "$(show_menu_option 3 "Optimize Mac - System health & tuning" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
|
||||||
printf '\r\033[2K%s\n' "$(show_menu_option 4 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 4 ]] && echo true || echo false)")"
|
printf '\r\033[2K%s\n' "$(show_menu_option 4 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 4 ]] && echo true || echo false)")"
|
||||||
|
printf '\r\033[2K%s\n' "$(show_menu_option 5 "System Status - Live CPU/GPU/memory" "$([[ $selected -eq 5 ]] && echo true || echo false)")"
|
||||||
|
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
printf '\r\033[2K\n'
|
printf '\r\033[2K\n'
|
||||||
@@ -566,7 +568,7 @@ interactive_main_menu() {
|
|||||||
|
|
||||||
case "$key" in
|
case "$key" in
|
||||||
"UP") ((current_option > 1)) && ((current_option--)) ;;
|
"UP") ((current_option > 1)) && ((current_option--)) ;;
|
||||||
"DOWN") ((current_option < 4)) && ((current_option++)) ;;
|
"DOWN") ((current_option < 5)) && ((current_option++)) ;;
|
||||||
"ENTER")
|
"ENTER")
|
||||||
show_cursor
|
show_cursor
|
||||||
case $current_option in
|
case $current_option in
|
||||||
@@ -574,6 +576,7 @@ interactive_main_menu() {
|
|||||||
2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;;
|
2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;;
|
||||||
3) exec "$SCRIPT_DIR/bin/optimize.sh" ;;
|
3) exec "$SCRIPT_DIR/bin/optimize.sh" ;;
|
||||||
4) exec "$SCRIPT_DIR/bin/analyze.sh" ;;
|
4) exec "$SCRIPT_DIR/bin/analyze.sh" ;;
|
||||||
|
5) exec "$SCRIPT_DIR/bin/status.sh" ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
"CHAR:1")
|
"CHAR:1")
|
||||||
@@ -592,6 +595,10 @@ interactive_main_menu() {
|
|||||||
show_cursor
|
show_cursor
|
||||||
exec "$SCRIPT_DIR/bin/analyze.sh"
|
exec "$SCRIPT_DIR/bin/analyze.sh"
|
||||||
;;
|
;;
|
||||||
|
"CHAR:5")
|
||||||
|
show_cursor
|
||||||
|
exec "$SCRIPT_DIR/bin/status.sh"
|
||||||
|
;;
|
||||||
"HELP")
|
"HELP")
|
||||||
show_cursor
|
show_cursor
|
||||||
clear
|
clear
|
||||||
@@ -626,6 +633,9 @@ main() {
|
|||||||
"analyze")
|
"analyze")
|
||||||
exec "$SCRIPT_DIR/bin/analyze.sh" "${@:2}"
|
exec "$SCRIPT_DIR/bin/analyze.sh" "${@:2}"
|
||||||
;;
|
;;
|
||||||
|
"status")
|
||||||
|
exec "$SCRIPT_DIR/bin/status.sh" "${@:2}"
|
||||||
|
;;
|
||||||
"touchid")
|
"touchid")
|
||||||
exec "$SCRIPT_DIR/bin/touchid.sh" "${@:2}"
|
exec "$SCRIPT_DIR/bin/touchid.sh" "${@:2}"
|
||||||
;;
|
;;
|
||||||
|
|||||||
44
scripts/build-status.sh
Executable file
44
scripts/build-status.sh
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build Universal Binary for status-go
|
||||||
|
# Supports both Apple Silicon and Intel Macs
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
if ! command -v go > /dev/null 2>&1; then
|
||||||
|
echo "Error: Go not installed"
|
||||||
|
echo "Install: brew install go"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building status-go for multiple architectures..."
|
||||||
|
|
||||||
|
VERSION=$(git describe --tags --always --dirty 2> /dev/null || echo "dev")
|
||||||
|
BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
LDFLAGS="-s -w -X main.Version=$VERSION -X main.BuildTime=$BUILD_TIME"
|
||||||
|
|
||||||
|
echo " Version: $VERSION"
|
||||||
|
echo " Build time: $BUILD_TIME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo " → Building for arm64..."
|
||||||
|
GOARCH=arm64 go build -ldflags="$LDFLAGS" -trimpath -o bin/status-go-arm64 ./cmd/status
|
||||||
|
|
||||||
|
echo " → Building for amd64..."
|
||||||
|
GOARCH=amd64 go build -ldflags="$LDFLAGS" -trimpath -o bin/status-go-amd64 ./cmd/status
|
||||||
|
|
||||||
|
echo " → Creating Universal Binary..."
|
||||||
|
lipo -create bin/status-go-arm64 bin/status-go-amd64 -output bin/status-go
|
||||||
|
|
||||||
|
rm bin/status-go-arm64 bin/status-go-amd64
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Build complete!"
|
||||||
|
echo ""
|
||||||
|
file bin/status-go
|
||||||
|
size_bytes=$(stat -f%z bin/status-go 2> /dev/null || echo 0)
|
||||||
|
size_mb=$((size_bytes / 1024 / 1024))
|
||||||
|
printf "Size: %d MB (%d bytes)\n" "$size_mb" "$size_bytes"
|
||||||
|
echo ""
|
||||||
|
echo "Binary supports: arm64 (Apple Silicon) + x86_64 (Intel)"
|
||||||
Reference in New Issue
Block a user