1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 17:24:45 +00:00

feat: Display system, adapter, and battery power metrics in status view

This commit is contained in:
Tw93
2025-12-22 19:30:35 +08:00
parent 81d4f7cb08
commit f410f356df
6 changed files with 96 additions and 34 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -118,10 +118,13 @@ type BatteryStatus struct {
} }
type ThermalStatus struct { type ThermalStatus struct {
CPUTemp float64 CPUTemp float64
GPUTemp float64 GPUTemp float64
FanSpeed int FanSpeed int
FanCount int FanCount int
SystemPower float64 // System power consumption in Watts
AdapterPower float64 // AC adapter max power in Watts
BatteryPower float64 // Battery charge/discharge power in Watts (positive = discharging)
} }
type SensorReading struct { type SensorReading struct {

View File

@@ -132,15 +132,13 @@ func getCachedPowerData() (health string, cycles int) {
for _, line := range lines { for _, line := range lines {
lower := strings.ToLower(line) lower := strings.ToLower(line)
if strings.Contains(lower, "cycle count") { if strings.Contains(lower, "cycle count") {
parts := strings.Split(line, ":") if _, after, found := strings.Cut(line, ":"); found {
if len(parts) == 2 { cycles, _ = strconv.Atoi(strings.TrimSpace(after))
cycles, _ = strconv.Atoi(strings.TrimSpace(parts[1]))
} }
} }
if strings.Contains(lower, "condition") { if strings.Contains(lower, "condition") {
parts := strings.Split(line, ":") if _, after, found := strings.Cut(line, ":"); found {
if len(parts) == 2 { health = strings.TrimSpace(after)
health = strings.TrimSpace(parts[1])
} }
} }
} }
@@ -175,44 +173,93 @@ func collectThermal() ThermalStatus {
var thermal ThermalStatus var thermal ThermalStatus
// Get fan info from cached system_profiler // Get fan info and adapter power from cached system_profiler
out := getSystemPowerOutput() out := getSystemPowerOutput()
if out != "" { if out != "" {
lines := strings.Split(out, "\n") lines := strings.Split(out, "\n")
for _, line := range lines { for _, line := range lines {
lower := strings.ToLower(line) lower := strings.ToLower(line)
if strings.Contains(lower, "fan") && strings.Contains(lower, "speed") { if strings.Contains(lower, "fan") && strings.Contains(lower, "speed") {
parts := strings.Split(line, ":") if _, after, found := strings.Cut(line, ":"); found {
if len(parts) == 2 {
// Extract number from string like "1200 RPM" // Extract number from string like "1200 RPM"
numStr := strings.TrimSpace(parts[1]) numStr := strings.TrimSpace(after)
numStr = strings.Split(numStr, " ")[0] numStr, _, _ = strings.Cut(numStr, " ")
thermal.FanSpeed, _ = strconv.Atoi(numStr) thermal.FanSpeed, _ = strconv.Atoi(numStr)
} }
} }
} }
} }
// 1. Try ioreg battery temperature (simple, no sudo needed) // Get power metrics from ioreg (fast, real-time data)
ctxIoreg, cancelIoreg := context.WithTimeout(context.Background(), 500*time.Millisecond) ctxPower, cancelPower := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancelIoreg() defer cancelPower()
if out, err := runCmd(ctxIoreg, "sh", "-c", "ioreg -rn AppleSmartBattery | awk '/\"Temperature\"/ {print $3}'"); err == nil { if out, err := runCmd(ctxPower, "ioreg", "-rn", "AppleSmartBattery"); err == nil {
valStr := strings.TrimSpace(out) lines := strings.Split(out, "\n")
if tempRaw, err := strconv.Atoi(valStr); err == nil && tempRaw > 0 { for _, line := range lines {
thermal.CPUTemp = float64(tempRaw) / 100.0 line = strings.TrimSpace(line)
return thermal
// Get battery temperature
// Matches: "Temperature" = 3055 (note: space before =)
if _, after, found := strings.Cut(line, "\"Temperature\" = "); found {
valStr := strings.TrimSpace(after)
if tempRaw, err := strconv.Atoi(valStr); err == nil && tempRaw > 0 {
thermal.CPUTemp = float64(tempRaw) / 100.0
}
}
// Get adapter power (Watts)
// Read from current adapter: "AdapterDetails" = {"Watts"=140...}
// Skip historical data: "AppleRawAdapterDetails" = ({Watts=90}, {Watts=140})
if strings.Contains(line, "\"AdapterDetails\" = {") && !strings.Contains(line, "AppleRaw") {
if _, after, found := strings.Cut(line, "\"Watts\"="); found {
valStr := strings.TrimSpace(after)
// Remove trailing characters like , or }
valStr, _, _ = strings.Cut(valStr, ",")
valStr, _, _ = strings.Cut(valStr, "}")
valStr = strings.TrimSpace(valStr)
if watts, err := strconv.ParseFloat(valStr, 64); err == nil && watts > 0 {
thermal.AdapterPower = watts
}
}
}
// Get system power consumption (mW -> W)
// Matches: "SystemPowerIn"=12345
if _, after, found := strings.Cut(line, "\"SystemPowerIn\"="); found {
valStr := strings.TrimSpace(after)
valStr, _, _ = strings.Cut(valStr, ",")
valStr, _, _ = strings.Cut(valStr, "}")
valStr = strings.TrimSpace(valStr)
if powerMW, err := strconv.ParseFloat(valStr, 64); err == nil && powerMW > 0 {
thermal.SystemPower = powerMW / 1000.0
}
}
// Get battery power (mW -> W, positive = discharging)
// Matches: "BatteryPower"=12345
if _, after, found := strings.Cut(line, "\"BatteryPower\"="); found {
valStr := strings.TrimSpace(after)
valStr, _, _ = strings.Cut(valStr, ",")
valStr, _, _ = strings.Cut(valStr, "}")
valStr = strings.TrimSpace(valStr)
if powerMW, err := strconv.ParseFloat(valStr, 64); err == nil {
thermal.BatteryPower = powerMW / 1000.0
}
}
} }
} }
// 2. Try thermal level as a proxy (fallback) // Fallback: Try thermal level as a proxy if temperature not found
ctx2, cancel2 := context.WithTimeout(context.Background(), 500*time.Millisecond) if thermal.CPUTemp == 0 {
defer cancel2() ctx2, cancel2 := context.WithTimeout(context.Background(), 500*time.Millisecond)
out2, err := runCmd(ctx2, "sysctl", "-n", "machdep.xcpm.cpu_thermal_level") defer cancel2()
if err == nil { out2, err := runCmd(ctx2, "sysctl", "-n", "machdep.xcpm.cpu_thermal_level")
level, _ := strconv.Atoi(strings.TrimSpace(out2)) if err == nil {
// Estimate temp: level 0-100 roughly maps to 40-100°C level, _ := strconv.Atoi(strings.TrimSpace(out2))
if level >= 0 { // Estimate temp: level 0-100 roughly maps to 40-100°C
thermal.CPUTemp = 45 + float64(level)*0.5 if level >= 0 {
thermal.CPUTemp = 45 + float64(level)*0.5
}
} }
} }

View File

@@ -520,7 +520,7 @@ func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData {
} }
lines = append(lines, fmt.Sprintf("Level %s %s", batteryProgressBar(b.Percent), percentText)) lines = append(lines, fmt.Sprintf("Level %s %s", batteryProgressBar(b.Percent), percentText))
// Line 2: status // Line 2: status with power info
statusIcon := "" statusIcon := ""
statusStyle := subtleStyle statusStyle := subtleStyle
if statusLower == "charging" || statusLower == "charged" { if statusLower == "charging" || statusLower == "charged" {
@@ -537,6 +537,18 @@ func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData {
if b.TimeLeft != "" { if b.TimeLeft != "" {
statusText += " · " + b.TimeLeft statusText += " · " + b.TimeLeft
} }
// Add power information
if statusLower == "charging" || statusLower == "charged" {
// AC powered - show system power consumption
if thermal.SystemPower > 0 {
statusText += fmt.Sprintf(" · %.0fW", thermal.SystemPower)
} else if thermal.AdapterPower > 0 {
statusText += fmt.Sprintf(" · %.0fW Adapter", thermal.AdapterPower)
}
} else if thermal.BatteryPower > 0 {
// Battery powered - show discharge rate
statusText += fmt.Sprintf(" · %.0fW", thermal.BatteryPower)
}
lines = append(lines, statusStyle.Render(statusText+statusIcon)) lines = append(lines, statusStyle.Render(statusText+statusIcon))
// Line 3: Health + cycles + temp // Line 3: Health + cycles + temp

2
mole
View File

@@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/core/common.sh" source "$SCRIPT_DIR/lib/core/common.sh"
# Version info # Version info
VERSION="1.14.1" VERSION="1.14.2"
MOLE_TAGLINE="Deep clean and optimize your Mac." MOLE_TAGLINE="Deep clean and optimize your Mac."
# Check TouchID configuration # Check TouchID configuration