mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 19:44:44 +00:00
675 lines
16 KiB
Go
675 lines
16 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/shirou/gopsutil/v3/cpu"
|
|
"github.com/shirou/gopsutil/v3/disk"
|
|
"github.com/shirou/gopsutil/v3/host"
|
|
"github.com/shirou/gopsutil/v3/mem"
|
|
"github.com/shirou/gopsutil/v3/net"
|
|
"github.com/shirou/gopsutil/v3/process"
|
|
)
|
|
|
|
// Styles
|
|
var (
|
|
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#C79FD7")).Bold(true)
|
|
headerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#87CEEB")).Bold(true)
|
|
labelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
|
valueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
|
|
okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#A5D6A7"))
|
|
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD75F"))
|
|
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F5F")).Bold(true)
|
|
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666"))
|
|
cardStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("#444444")).Padding(0, 1)
|
|
)
|
|
|
|
// Metrics snapshot
|
|
type MetricsSnapshot struct {
|
|
CollectedAt time.Time
|
|
HealthScore int
|
|
HealthMessage string
|
|
|
|
// Hardware
|
|
Hostname string
|
|
OS string
|
|
Platform string
|
|
Uptime time.Duration
|
|
|
|
// CPU
|
|
CPUModel string
|
|
CPUCores int
|
|
CPUPercent float64
|
|
CPUPerCore []float64
|
|
|
|
// Memory
|
|
MemTotal uint64
|
|
MemUsed uint64
|
|
MemPercent float64
|
|
SwapTotal uint64
|
|
SwapUsed uint64
|
|
SwapPercent float64
|
|
|
|
// Disk
|
|
Disks []DiskInfo
|
|
|
|
// Network
|
|
Networks []NetworkInfo
|
|
|
|
// Processes
|
|
TopProcesses []ProcessInfo
|
|
}
|
|
|
|
type DiskInfo struct {
|
|
Device string
|
|
Mountpoint string
|
|
Total uint64
|
|
Used uint64
|
|
Free uint64
|
|
UsedPercent float64
|
|
Fstype string
|
|
}
|
|
|
|
type NetworkInfo struct {
|
|
Name string
|
|
BytesSent uint64
|
|
BytesRecv uint64
|
|
PacketsSent uint64
|
|
PacketsRecv uint64
|
|
}
|
|
|
|
type ProcessInfo struct {
|
|
PID int32
|
|
Name string
|
|
CPU float64
|
|
Memory float32
|
|
}
|
|
|
|
// Collector
|
|
type Collector struct {
|
|
prevNet map[string]net.IOCountersStat
|
|
prevNetTime time.Time
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func NewCollector() *Collector {
|
|
return &Collector{
|
|
prevNet: make(map[string]net.IOCountersStat),
|
|
}
|
|
}
|
|
|
|
func (c *Collector) Collect() MetricsSnapshot {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
var (
|
|
snapshot MetricsSnapshot
|
|
wg sync.WaitGroup
|
|
mu sync.Mutex
|
|
)
|
|
|
|
snapshot.CollectedAt = time.Now()
|
|
|
|
// Host info
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if info, err := host.InfoWithContext(ctx); err == nil {
|
|
mu.Lock()
|
|
snapshot.Hostname = info.Hostname
|
|
snapshot.OS = info.OS
|
|
snapshot.Platform = fmt.Sprintf("%s %s", info.Platform, info.PlatformVersion)
|
|
snapshot.Uptime = time.Duration(info.Uptime) * time.Second
|
|
mu.Unlock()
|
|
}
|
|
}()
|
|
|
|
// CPU info
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if cpuInfo, err := cpu.InfoWithContext(ctx); err == nil && len(cpuInfo) > 0 {
|
|
mu.Lock()
|
|
snapshot.CPUModel = cpuInfo[0].ModelName
|
|
snapshot.CPUCores = runtime.NumCPU()
|
|
mu.Unlock()
|
|
}
|
|
if percent, err := cpu.PercentWithContext(ctx, 500*time.Millisecond, false); err == nil && len(percent) > 0 {
|
|
mu.Lock()
|
|
snapshot.CPUPercent = percent[0]
|
|
mu.Unlock()
|
|
}
|
|
if perCore, err := cpu.PercentWithContext(ctx, 500*time.Millisecond, true); err == nil {
|
|
mu.Lock()
|
|
snapshot.CPUPerCore = perCore
|
|
mu.Unlock()
|
|
}
|
|
}()
|
|
|
|
// Memory
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if memInfo, err := mem.VirtualMemoryWithContext(ctx); err == nil {
|
|
mu.Lock()
|
|
snapshot.MemTotal = memInfo.Total
|
|
snapshot.MemUsed = memInfo.Used
|
|
snapshot.MemPercent = memInfo.UsedPercent
|
|
mu.Unlock()
|
|
}
|
|
if swapInfo, err := mem.SwapMemoryWithContext(ctx); err == nil {
|
|
mu.Lock()
|
|
snapshot.SwapTotal = swapInfo.Total
|
|
snapshot.SwapUsed = swapInfo.Used
|
|
snapshot.SwapPercent = swapInfo.UsedPercent
|
|
mu.Unlock()
|
|
}
|
|
}()
|
|
|
|
// Disk
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if partitions, err := disk.PartitionsWithContext(ctx, false); err == nil {
|
|
var disks []DiskInfo
|
|
for _, p := range partitions {
|
|
// Skip non-physical drives
|
|
if !strings.HasPrefix(p.Device, "C:") &&
|
|
!strings.HasPrefix(p.Device, "D:") &&
|
|
!strings.HasPrefix(p.Device, "E:") &&
|
|
!strings.HasPrefix(p.Device, "F:") {
|
|
continue
|
|
}
|
|
if usage, err := disk.UsageWithContext(ctx, p.Mountpoint); err == nil {
|
|
disks = append(disks, DiskInfo{
|
|
Device: p.Device,
|
|
Mountpoint: p.Mountpoint,
|
|
Total: usage.Total,
|
|
Used: usage.Used,
|
|
Free: usage.Free,
|
|
UsedPercent: usage.UsedPercent,
|
|
Fstype: p.Fstype,
|
|
})
|
|
}
|
|
}
|
|
mu.Lock()
|
|
snapshot.Disks = disks
|
|
mu.Unlock()
|
|
}
|
|
}()
|
|
|
|
// Network
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if netIO, err := net.IOCountersWithContext(ctx, true); err == nil {
|
|
var networks []NetworkInfo
|
|
for _, io := range netIO {
|
|
// Skip loopback and inactive interfaces
|
|
if io.Name == "Loopback Pseudo-Interface 1" || (io.BytesSent == 0 && io.BytesRecv == 0) {
|
|
continue
|
|
}
|
|
networks = append(networks, NetworkInfo{
|
|
Name: io.Name,
|
|
BytesSent: io.BytesSent,
|
|
BytesRecv: io.BytesRecv,
|
|
PacketsSent: io.PacketsSent,
|
|
PacketsRecv: io.PacketsRecv,
|
|
})
|
|
}
|
|
mu.Lock()
|
|
snapshot.Networks = networks
|
|
mu.Unlock()
|
|
}
|
|
}()
|
|
|
|
// Top Processes
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
procs, err := process.ProcessesWithContext(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var procInfos []ProcessInfo
|
|
for _, p := range procs {
|
|
name, err := p.NameWithContext(ctx)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
cpuPercent, _ := p.CPUPercentWithContext(ctx)
|
|
memPercent, _ := p.MemoryPercentWithContext(ctx)
|
|
|
|
if cpuPercent > 0.1 || memPercent > 0.1 {
|
|
procInfos = append(procInfos, ProcessInfo{
|
|
PID: p.Pid,
|
|
Name: name,
|
|
CPU: cpuPercent,
|
|
Memory: memPercent,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sort by CPU usage
|
|
for i := 0; i < len(procInfos)-1; i++ {
|
|
for j := i + 1; j < len(procInfos); j++ {
|
|
if procInfos[j].CPU > procInfos[i].CPU {
|
|
procInfos[i], procInfos[j] = procInfos[j], procInfos[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Take top 5
|
|
if len(procInfos) > 5 {
|
|
procInfos = procInfos[:5]
|
|
}
|
|
|
|
mu.Lock()
|
|
snapshot.TopProcesses = procInfos
|
|
mu.Unlock()
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
// Calculate health score
|
|
snapshot.HealthScore, snapshot.HealthMessage = calculateHealthScore(snapshot)
|
|
|
|
return snapshot
|
|
}
|
|
|
|
func calculateHealthScore(s MetricsSnapshot) (int, string) {
|
|
score := 100
|
|
var issues []string
|
|
|
|
// CPU penalty (30% weight)
|
|
if s.CPUPercent > 90 {
|
|
score -= 30
|
|
issues = append(issues, "High CPU")
|
|
} else if s.CPUPercent > 70 {
|
|
score -= 15
|
|
issues = append(issues, "Elevated CPU")
|
|
}
|
|
|
|
// Memory penalty (25% weight)
|
|
if s.MemPercent > 90 {
|
|
score -= 25
|
|
issues = append(issues, "High Memory")
|
|
} else if s.MemPercent > 80 {
|
|
score -= 12
|
|
issues = append(issues, "Elevated Memory")
|
|
}
|
|
|
|
// Disk penalty (20% weight)
|
|
for _, d := range s.Disks {
|
|
if d.UsedPercent > 95 {
|
|
score -= 20
|
|
issues = append(issues, fmt.Sprintf("Disk %s Critical", d.Device))
|
|
break
|
|
} else if d.UsedPercent > 85 {
|
|
score -= 10
|
|
issues = append(issues, fmt.Sprintf("Disk %s Low", d.Device))
|
|
break
|
|
}
|
|
}
|
|
|
|
// Swap penalty (10% weight)
|
|
if s.SwapPercent > 80 {
|
|
score -= 10
|
|
issues = append(issues, "High Swap")
|
|
}
|
|
|
|
if score < 0 {
|
|
score = 0
|
|
}
|
|
|
|
msg := "Excellent"
|
|
if len(issues) > 0 {
|
|
msg = strings.Join(issues, ", ")
|
|
} else if score >= 90 {
|
|
msg = "Excellent"
|
|
} else if score >= 70 {
|
|
msg = "Good"
|
|
} else if score >= 50 {
|
|
msg = "Fair"
|
|
} else {
|
|
msg = "Poor"
|
|
}
|
|
|
|
return score, msg
|
|
}
|
|
|
|
// Model for Bubble Tea
|
|
type model struct {
|
|
collector *Collector
|
|
metrics MetricsSnapshot
|
|
animFrame int
|
|
catHidden bool
|
|
ready bool
|
|
collecting bool
|
|
width int
|
|
height int
|
|
}
|
|
|
|
// Messages
|
|
type tickMsg time.Time
|
|
type metricsMsg MetricsSnapshot
|
|
|
|
func newModel() model {
|
|
return model{
|
|
collector: NewCollector(),
|
|
animFrame: 0,
|
|
}
|
|
}
|
|
|
|
func (m model) Init() tea.Cmd {
|
|
return tea.Batch(
|
|
m.collectMetrics(),
|
|
tickCmd(),
|
|
)
|
|
}
|
|
|
|
func tickCmd() tea.Cmd {
|
|
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
|
|
return tickMsg(t)
|
|
})
|
|
}
|
|
|
|
func (m model) collectMetrics() tea.Cmd {
|
|
return func() tea.Msg {
|
|
return metricsMsg(m.collector.Collect())
|
|
}
|
|
}
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "q", "ctrl+c":
|
|
return m, tea.Quit
|
|
case "c":
|
|
m.catHidden = !m.catHidden
|
|
case "r":
|
|
m.collecting = true
|
|
return m, m.collectMetrics()
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
m.width = msg.Width
|
|
m.height = msg.Height
|
|
case tickMsg:
|
|
m.animFrame++
|
|
if m.animFrame%2 == 0 && !m.collecting {
|
|
return m, tea.Batch(
|
|
m.collectMetrics(),
|
|
tickCmd(),
|
|
)
|
|
}
|
|
return m, tickCmd()
|
|
case metricsMsg:
|
|
m.metrics = MetricsSnapshot(msg)
|
|
m.ready = true
|
|
m.collecting = false
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m model) View() string {
|
|
if !m.ready {
|
|
return "\n Loading system metrics..."
|
|
}
|
|
|
|
var b strings.Builder
|
|
|
|
// Header with mole animation
|
|
moleFrame := getMoleFrame(m.animFrame, m.catHidden)
|
|
|
|
b.WriteString("\n")
|
|
b.WriteString(titleStyle.Render(" 🐹 Mole System Status"))
|
|
b.WriteString(" ")
|
|
b.WriteString(moleFrame)
|
|
b.WriteString("\n\n")
|
|
|
|
// Health score
|
|
healthColor := okStyle
|
|
if m.metrics.HealthScore < 50 {
|
|
healthColor = dangerStyle
|
|
} else if m.metrics.HealthScore < 70 {
|
|
healthColor = warnStyle
|
|
}
|
|
b.WriteString(fmt.Sprintf(" Health: %s %s\n\n",
|
|
healthColor.Render(fmt.Sprintf("%d%%", m.metrics.HealthScore)),
|
|
dimStyle.Render(m.metrics.HealthMessage),
|
|
))
|
|
|
|
// System info
|
|
b.WriteString(headerStyle.Render(" 📍 System"))
|
|
b.WriteString("\n")
|
|
b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Host:"), valueStyle.Render(m.metrics.Hostname)))
|
|
b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("OS:"), valueStyle.Render(m.metrics.Platform)))
|
|
b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Uptime:"), valueStyle.Render(formatDuration(m.metrics.Uptime))))
|
|
b.WriteString("\n")
|
|
|
|
// CPU
|
|
b.WriteString(headerStyle.Render(" ⚡ CPU"))
|
|
b.WriteString("\n")
|
|
cpuColor := getPercentColor(m.metrics.CPUPercent)
|
|
b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Model:"), valueStyle.Render(truncateString(m.metrics.CPUModel, 50))))
|
|
b.WriteString(fmt.Sprintf(" %s %s (%d cores)\n",
|
|
labelStyle.Render("Usage:"),
|
|
cpuColor.Render(fmt.Sprintf("%.1f%%", m.metrics.CPUPercent)),
|
|
m.metrics.CPUCores,
|
|
))
|
|
b.WriteString(fmt.Sprintf(" %s\n", renderProgressBar(m.metrics.CPUPercent, 30)))
|
|
b.WriteString("\n")
|
|
|
|
// Memory
|
|
b.WriteString(headerStyle.Render(" 🧠 Memory"))
|
|
b.WriteString("\n")
|
|
memColor := getPercentColor(m.metrics.MemPercent)
|
|
b.WriteString(fmt.Sprintf(" %s %s / %s %s\n",
|
|
labelStyle.Render("RAM:"),
|
|
memColor.Render(formatBytes(m.metrics.MemUsed)),
|
|
valueStyle.Render(formatBytes(m.metrics.MemTotal)),
|
|
memColor.Render(fmt.Sprintf("(%.1f%%)", m.metrics.MemPercent)),
|
|
))
|
|
b.WriteString(fmt.Sprintf(" %s\n", renderProgressBar(m.metrics.MemPercent, 30)))
|
|
if m.metrics.SwapTotal > 0 {
|
|
b.WriteString(fmt.Sprintf(" %s %s / %s\n",
|
|
labelStyle.Render("Swap:"),
|
|
valueStyle.Render(formatBytes(m.metrics.SwapUsed)),
|
|
valueStyle.Render(formatBytes(m.metrics.SwapTotal)),
|
|
))
|
|
}
|
|
b.WriteString("\n")
|
|
|
|
// Disk
|
|
b.WriteString(headerStyle.Render(" 💾 Disks"))
|
|
b.WriteString("\n")
|
|
for _, d := range m.metrics.Disks {
|
|
diskColor := getPercentColor(d.UsedPercent)
|
|
b.WriteString(fmt.Sprintf(" %s %s / %s %s\n",
|
|
labelStyle.Render(d.Device),
|
|
diskColor.Render(formatBytes(d.Used)),
|
|
valueStyle.Render(formatBytes(d.Total)),
|
|
diskColor.Render(fmt.Sprintf("(%.1f%%)", d.UsedPercent)),
|
|
))
|
|
b.WriteString(fmt.Sprintf(" %s\n", renderProgressBar(d.UsedPercent, 30)))
|
|
}
|
|
b.WriteString("\n")
|
|
|
|
// Top Processes
|
|
if len(m.metrics.TopProcesses) > 0 {
|
|
b.WriteString(headerStyle.Render(" 📊 Top Processes"))
|
|
b.WriteString("\n")
|
|
for _, p := range m.metrics.TopProcesses {
|
|
b.WriteString(fmt.Sprintf(" %s %s (CPU: %.1f%%, Mem: %.1f%%)\n",
|
|
dimStyle.Render(fmt.Sprintf("[%d]", p.PID)),
|
|
valueStyle.Render(truncateString(p.Name, 20)),
|
|
p.CPU,
|
|
p.Memory,
|
|
))
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
// Network
|
|
if len(m.metrics.Networks) > 0 {
|
|
b.WriteString(headerStyle.Render(" 🌐 Network"))
|
|
b.WriteString("\n")
|
|
for i, n := range m.metrics.Networks {
|
|
if i >= 3 {
|
|
break
|
|
}
|
|
b.WriteString(fmt.Sprintf(" %s ↑%s ↓%s\n",
|
|
labelStyle.Render(truncateString(n.Name, 20)+":"),
|
|
valueStyle.Render(formatBytes(n.BytesSent)),
|
|
valueStyle.Render(formatBytes(n.BytesRecv)),
|
|
))
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
// Footer
|
|
b.WriteString(dimStyle.Render(" [q] quit [r] refresh [c] toggle mole"))
|
|
b.WriteString("\n")
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func getMoleFrame(frame int, hidden bool) string {
|
|
if hidden {
|
|
return ""
|
|
}
|
|
frames := []string{
|
|
"🐹",
|
|
"🐹.",
|
|
"🐹..",
|
|
"🐹...",
|
|
}
|
|
return frames[frame%len(frames)]
|
|
}
|
|
|
|
func renderProgressBar(percent float64, width int) string {
|
|
filled := int(percent / 100 * float64(width))
|
|
if filled > width {
|
|
filled = width
|
|
}
|
|
if filled < 0 {
|
|
filled = 0
|
|
}
|
|
|
|
color := okStyle
|
|
if percent > 85 {
|
|
color = dangerStyle
|
|
} else if percent > 70 {
|
|
color = warnStyle
|
|
}
|
|
|
|
bar := strings.Repeat("█", filled) + strings.Repeat("░", width-filled)
|
|
return color.Render(bar)
|
|
}
|
|
|
|
func getPercentColor(percent float64) lipgloss.Style {
|
|
if percent > 85 {
|
|
return dangerStyle
|
|
} else if percent > 70 {
|
|
return warnStyle
|
|
}
|
|
return okStyle
|
|
}
|
|
|
|
func formatBytes(bytes uint64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := uint64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
func formatDuration(d time.Duration) string {
|
|
days := int(d.Hours() / 24)
|
|
hours := int(d.Hours()) % 24
|
|
minutes := int(d.Minutes()) % 60
|
|
|
|
if days > 0 {
|
|
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
|
|
}
|
|
if hours > 0 {
|
|
return fmt.Sprintf("%dh %dm", hours, minutes)
|
|
}
|
|
return fmt.Sprintf("%dm", minutes)
|
|
}
|
|
|
|
func truncateString(s string, maxLen int) string {
|
|
if len(s) <= maxLen {
|
|
return s
|
|
}
|
|
return s[:maxLen-3] + "..."
|
|
}
|
|
|
|
// getWindowsVersion gets detailed Windows version using PowerShell
|
|
func getWindowsVersion() string {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, "powershell", "-Command",
|
|
"(Get-CimInstance Win32_OperatingSystem).Caption")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return "Windows"
|
|
}
|
|
return strings.TrimSpace(string(output))
|
|
}
|
|
|
|
// getBatteryInfo gets battery info on Windows (for laptops)
|
|
func getBatteryInfo() (int, bool, bool) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, "powershell", "-Command",
|
|
"(Get-CimInstance Win32_Battery).EstimatedChargeRemaining")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return 0, false, false
|
|
}
|
|
|
|
percent, err := strconv.Atoi(strings.TrimSpace(string(output)))
|
|
if err != nil {
|
|
return 0, false, false
|
|
}
|
|
|
|
// Check if charging
|
|
cmdStatus := exec.CommandContext(ctx, "powershell", "-Command",
|
|
"(Get-CimInstance Win32_Battery).BatteryStatus")
|
|
statusOutput, _ := cmdStatus.Output()
|
|
status, _ := strconv.Atoi(strings.TrimSpace(string(statusOutput)))
|
|
isCharging := status == 2 // 2 = AC Power
|
|
|
|
return percent, isCharging, true
|
|
}
|
|
|
|
func main() {
|
|
p := tea.NewProgram(newModel(), tea.WithAltScreen())
|
|
if _, err := p.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|