From 80757ec074dd2acd31f777604b124f007a1d38da Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Tue, 6 Jan 2026 11:41:35 +0200 Subject: [PATCH 1/7] refactor: replace deprecated `Start` with `Run` --- cmd/analyze/main.go | 2 +- cmd/status/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/analyze/main.go b/cmd/analyze/main.go index f81055b..2c84d0e 100644 --- a/cmd/analyze/main.go +++ b/cmd/analyze/main.go @@ -148,7 +148,7 @@ func main() { go prefetchOverviewCache(prefetchCtx) p := tea.NewProgram(newModel(abs, isOverview), tea.WithAltScreen()) - if err := p.Start(); err != nil { + if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, "analyzer error: %v\n", err) os.Exit(1) } diff --git a/cmd/status/main.go b/cmd/status/main.go index 4e152ee..7a9da88 100644 --- a/cmd/status/main.go +++ b/cmd/status/main.go @@ -136,7 +136,7 @@ func animTickWithSpeed(cpuUsage float64) tea.Cmd { func main() { p := tea.NewProgram(newModel(), tea.WithAltScreen()) - if err := p.Start(); err != nil { + if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, "system status error: %v\n", err) os.Exit(1) } From 158af1e1ba0e417054a0e735a619c33892e53b9c Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Tue, 6 Jan 2026 11:52:05 +0200 Subject: [PATCH 2/7] refactor: modernize Go code --- cmd/analyze/format.go | 32 +++++++++++++++---------------- cmd/analyze/heap.go | 8 ++++---- cmd/analyze/main.go | 12 +++--------- cmd/analyze/scanner.go | 22 ++++++--------------- cmd/analyze/view.go | 25 +++++------------------- cmd/status/main.go | 5 +---- cmd/status/metrics_battery.go | 12 ++++-------- cmd/status/metrics_bluetooth.go | 10 ++++------ cmd/status/metrics_disk.go | 2 +- cmd/status/metrics_gpu.go | 3 +-- cmd/status/metrics_hardware.go | 9 +++------ cmd/status/view.go | 34 ++++++++------------------------- 12 files changed, 55 insertions(+), 119 deletions(-) diff --git a/cmd/analyze/format.go b/cmd/analyze/format.go index 7686745..2350879 100644 --- a/cmd/analyze/format.go +++ b/cmd/analyze/format.go @@ -93,15 +93,12 @@ func humanizeBytes(size int64) string { return fmt.Sprintf("%.1f %cB", value, "KMGTPE"[exp]) } -func coloredProgressBar(value, max int64, percent float64) string { - if max <= 0 { +func coloredProgressBar(value, maxValue int64, percent float64) string { + if maxValue <= 0 { return colorGray + strings.Repeat("░", barWidth) + colorReset } - filled := int((value * int64(barWidth)) / max) - if filled > barWidth { - filled = barWidth - } + filled := min(int((value*int64(barWidth))/maxValue), barWidth) var barColor string if percent >= 50 { @@ -114,26 +111,27 @@ func coloredProgressBar(value, max int64, percent float64) string { barColor = colorGreen } - bar := barColor - for i := 0; i < barWidth; i++ { + var bar strings.Builder + bar.WriteString(barColor) + for i := range barWidth { if i < filled { if i < filled-1 { - bar += "█" + bar.WriteString("█") } else { - remainder := (value * int64(barWidth)) % max - if remainder > max/2 { - bar += "█" - } else if remainder > max/4 { - bar += "▓" + remainder := (value * int64(barWidth)) % maxValue + if remainder > maxValue/2 { + bar.WriteString("█") + } else if remainder > maxValue/4 { + bar.WriteString("▓") } else { - bar += "▒" + bar.WriteString("▒") } } } else { - bar += colorGray + "░" + barColor + bar.WriteString(colorGray + "░" + barColor) } } - return bar + colorReset + return bar.String() + colorReset } // runeWidth returns display width for wide characters and emoji. diff --git a/cmd/analyze/heap.go b/cmd/analyze/heap.go index 08bf8a9..0b4a5a5 100644 --- a/cmd/analyze/heap.go +++ b/cmd/analyze/heap.go @@ -7,11 +7,11 @@ func (h entryHeap) Len() int { return len(h) } func (h entryHeap) Less(i, j int) bool { return h[i].Size < h[j].Size } func (h entryHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } -func (h *entryHeap) Push(x interface{}) { +func (h *entryHeap) Push(x any) { *h = append(*h, x.(dirEntry)) } -func (h *entryHeap) Pop() interface{} { +func (h *entryHeap) Pop() any { old := *h n := len(old) x := old[n-1] @@ -26,11 +26,11 @@ func (h largeFileHeap) Len() int { return len(h) } func (h largeFileHeap) Less(i, j int) bool { return h[i].Size < h[j].Size } func (h largeFileHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } -func (h *largeFileHeap) Push(x interface{}) { +func (h *largeFileHeap) Push(x any) { *h = append(*h, x.(fileEntry)) } -func (h *largeFileHeap) Pop() interface{} { +func (h *largeFileHeap) Pop() any { old := *h n := len(old) x := old[n-1] diff --git a/cmd/analyze/main.go b/cmd/analyze/main.go index f81055b..c44476e 100644 --- a/cmd/analyze/main.go +++ b/cmd/analyze/main.go @@ -359,7 +359,7 @@ func (m model) scanCmd(path string) tea.Cmd { return scanResultMsg{result: result, err: nil} } - v, err, _ := scanGroup.Do(path, func() (interface{}, error) { + v, err, _ := scanGroup.Do(path, func() (any, error) { return scanPathConcurrent(path, m.filesScanned, m.dirsScanned, m.bytesScanned, m.currentPath) }) @@ -997,10 +997,7 @@ func (m *model) clampEntrySelection() { m.selected = 0 } viewport := calculateViewport(m.height, false) - maxOffset := len(m.entries) - viewport - if maxOffset < 0 { - maxOffset = 0 - } + maxOffset := max(len(m.entries)-viewport, 0) if m.offset > maxOffset { m.offset = maxOffset } @@ -1025,10 +1022,7 @@ func (m *model) clampLargeSelection() { m.largeSelected = 0 } viewport := calculateViewport(m.height, true) - maxOffset := len(m.largeFiles) - viewport - if maxOffset < 0 { - maxOffset = 0 - } + maxOffset := max(len(m.largeFiles)-viewport, 0) if m.largeOffset > maxOffset { m.largeOffset = maxOffset } diff --git a/cmd/analyze/scanner.go b/cmd/analyze/scanner.go index 9654e59..b6ab09b 100644 --- a/cmd/analyze/scanner.go +++ b/cmd/analyze/scanner.go @@ -39,10 +39,7 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in heap.Init(largeFilesHeap) // Worker pool sized for I/O-bound scanning. - numWorkers := runtime.NumCPU() * cpuMultiplier - if numWorkers < minWorkers { - numWorkers = minWorkers - } + numWorkers := max(runtime.NumCPU()*cpuMultiplier, minWorkers) if numWorkers > maxWorkers { numWorkers = maxWorkers } @@ -289,10 +286,7 @@ func calculateDirSizeFast(root string, filesScanned, dirsScanned, bytesScanned * ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - concurrency := runtime.NumCPU() * 4 - if concurrency > 64 { - concurrency = 64 - } + concurrency := min(runtime.NumCPU()*4, 64) sem := make(chan struct{}, concurrency) var walk func(string) @@ -363,10 +357,9 @@ func findLargeFilesWithSpotlight(root string, minSize int64) []fileEntry { return nil } - lines := strings.Split(strings.TrimSpace(string(output)), "\n") var files []fileEntry - for _, line := range lines { + for line := range strings.Lines(strings.TrimSpace(string(output))) { if line == "" { continue } @@ -413,8 +406,8 @@ func findLargeFilesWithSpotlight(root string, minSize int64) []fileEntry { // isInFoldedDir checks if a path is inside a folded directory. func isInFoldedDir(path string) bool { - parts := strings.Split(path, string(os.PathSeparator)) - for _, part := range parts { + parts := strings.SplitSeq(path, string(os.PathSeparator)) + for part := range parts { if foldDirs[part] { return true } @@ -432,10 +425,7 @@ func calculateDirSizeConcurrent(root string, largeFileChan chan<- fileEntry, fil var wg sync.WaitGroup // Limit concurrent subdirectory scans. - maxConcurrent := runtime.NumCPU() * 2 - if maxConcurrent > maxDirWorkers { - maxConcurrent = maxDirWorkers - } + maxConcurrent := min(runtime.NumCPU()*2, maxDirWorkers) sem := make(chan struct{}, maxConcurrent) for _, child := range children { diff --git a/cmd/analyze/view.go b/cmd/analyze/view.go index a434c71..6cdf400 100644 --- a/cmd/analyze/view.go +++ b/cmd/analyze/view.go @@ -100,14 +100,8 @@ func (m model) View() string { fmt.Fprintln(&b, " No large files found (>=100MB)") } else { viewport := calculateViewport(m.height, true) - start := m.largeOffset - if start < 0 { - start = 0 - } - end := start + viewport - if end > len(m.largeFiles) { - end = len(m.largeFiles) - } + start := max(m.largeOffset, 0) + end := min(start+viewport, len(m.largeFiles)) maxLargeSize := int64(1) for _, file := range m.largeFiles { if file.Size > maxLargeSize { @@ -163,10 +157,7 @@ func (m model) View() string { for idx, entry := range m.entries { icon := "📁" sizeVal := entry.Size - barValue := sizeVal - if barValue < 0 { - barValue = 0 - } + barValue := max(sizeVal, 0) var percent float64 if totalSize > 0 && sizeVal >= 0 { percent = float64(sizeVal) / float64(totalSize) * 100 @@ -243,14 +234,8 @@ func (m model) View() string { viewport := calculateViewport(m.height, false) nameWidth := calculateNameWidth(m.width) - start := m.offset - if start < 0 { - start = 0 - } - end := start + viewport - if end > len(m.entries) { - end = len(m.entries) - } + start := max(m.offset, 0) + end := min(start+viewport, len(m.entries)) for idx := start; idx < end; idx++ { entry := m.entries[idx] diff --git a/cmd/status/main.go b/cmd/status/main.go index 4e152ee..9af9fc0 100644 --- a/cmd/status/main.go +++ b/cmd/status/main.go @@ -127,10 +127,7 @@ func animTick() tea.Cmd { func animTickWithSpeed(cpuUsage float64) tea.Cmd { // Higher CPU = faster animation. - interval := 300 - int(cpuUsage*2.5) - if interval < 50 { - interval = 50 - } + interval := max(300-int(cpuUsage*2.5), 50) return tea.Tick(time.Duration(interval)*time.Millisecond, func(time.Time) tea.Msg { return animTickMsg{} }) } diff --git a/cmd/status/metrics_battery.go b/cmd/status/metrics_battery.go index ecdb463..ef3515d 100644 --- a/cmd/status/metrics_battery.go +++ b/cmd/status/metrics_battery.go @@ -68,11 +68,10 @@ func collectBatteries() (batts []BatteryStatus, err error) { } func parsePMSet(raw string, health string, cycles int, capacity int) []BatteryStatus { - lines := strings.Split(raw, "\n") var out []BatteryStatus var timeLeft string - for _, line := range lines { + for line := range strings.Lines(raw) { // Time remaining. if strings.Contains(line, "remaining") { parts := strings.Fields(line) @@ -128,8 +127,7 @@ func getCachedPowerData() (health string, cycles int, capacity int) { return "", 0, 0 } - lines := strings.Split(out, "\n") - for _, line := range lines { + for line := range strings.Lines(out) { lower := strings.ToLower(line) if strings.Contains(lower, "cycle count") { if _, after, found := strings.Cut(line, ":"); found { @@ -183,8 +181,7 @@ func collectThermal() ThermalStatus { // Fan info from cached system_profiler. out := getSystemPowerOutput() if out != "" { - lines := strings.Split(out, "\n") - for _, line := range lines { + for line := range strings.Lines(out) { lower := strings.ToLower(line) if strings.Contains(lower, "fan") && strings.Contains(lower, "speed") { if _, after, found := strings.Cut(line, ":"); found { @@ -200,8 +197,7 @@ func collectThermal() ThermalStatus { ctxPower, cancelPower := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancelPower() if out, err := runCmd(ctxPower, "ioreg", "-rn", "AppleSmartBattery"); err == nil { - lines := strings.Split(out, "\n") - for _, line := range lines { + for line := range strings.Lines(out) { line = strings.TrimSpace(line) // Battery temperature ("Temperature" = 3055). diff --git a/cmd/status/metrics_bluetooth.go b/cmd/status/metrics_bluetooth.go index f0c35a7..740c10c 100644 --- a/cmd/status/metrics_bluetooth.go +++ b/cmd/status/metrics_bluetooth.go @@ -68,13 +68,12 @@ func readBluetoothCTLDevices() ([]BluetoothDevice, error) { } func parseSPBluetooth(raw string) []BluetoothDevice { - lines := strings.Split(raw, "\n") var devices []BluetoothDevice var currentName string var connected bool var battery string - for _, line := range lines { + for line := range strings.Lines(raw) { trim := strings.TrimSpace(line) if len(trim) == 0 { continue @@ -112,10 +111,9 @@ func parseSPBluetooth(raw string) []BluetoothDevice { } func parseBluetoothctl(raw string) []BluetoothDevice { - lines := strings.Split(raw, "\n") var devices []BluetoothDevice current := BluetoothDevice{} - for _, line := range lines { + for line := range strings.Lines(raw) { trim := strings.TrimSpace(line) if strings.HasPrefix(trim, "Device ") { if current.Name != "" { @@ -123,8 +121,8 @@ func parseBluetoothctl(raw string) []BluetoothDevice { } current = BluetoothDevice{Name: strings.TrimPrefix(trim, "Device "), Connected: false} } - if strings.HasPrefix(trim, "Name:") { - current.Name = strings.TrimSpace(strings.TrimPrefix(trim, "Name:")) + if after, ok := strings.CutPrefix(trim, "Name:"); ok { + current.Name = strings.TrimSpace(after) } if strings.HasPrefix(trim, "Connected:") { current.Connected = strings.Contains(trim, "yes") diff --git a/cmd/status/metrics_disk.go b/cmd/status/metrics_disk.go index 3863aa0..9586fae 100644 --- a/cmd/status/metrics_disk.go +++ b/cmd/status/metrics_disk.go @@ -156,7 +156,7 @@ func isExternalDisk(device string) (bool, error) { found bool external bool ) - for _, line := range strings.Split(out, "\n") { + for line := range strings.Lines(out) { trim := strings.TrimSpace(line) if strings.HasPrefix(trim, "Internal:") { found = true diff --git a/cmd/status/metrics_gpu.go b/cmd/status/metrics_gpu.go index d4775f2..bb60235 100644 --- a/cmd/status/metrics_gpu.go +++ b/cmd/status/metrics_gpu.go @@ -61,9 +61,8 @@ func (c *Collector) collectGPU(now time.Time) ([]GPUStatus, error) { return nil, err } - lines := strings.Split(strings.TrimSpace(out), "\n") var gpus []GPUStatus - for _, line := range lines { + for line := range strings.Lines(strings.TrimSpace(out)) { fields := strings.Split(line, ",") if len(fields) < 4 { continue diff --git a/cmd/status/metrics_hardware.go b/cmd/status/metrics_hardware.go index 731d6a7..8117e57 100644 --- a/cmd/status/metrics_hardware.go +++ b/cmd/status/metrics_hardware.go @@ -28,8 +28,7 @@ func collectHardware(totalRAM uint64, disks []DiskStatus) HardwareInfo { out, err := runCmd(ctx, "system_profiler", "SPHardwareDataType") if err == nil { - lines := strings.Split(out, "\n") - for _, line := range lines { + for line := range strings.Lines(out) { lower := strings.ToLower(strings.TrimSpace(line)) // Prefer "Model Name" over "Model Identifier". if strings.Contains(lower, "model name:") { @@ -85,10 +84,9 @@ func collectHardware(totalRAM uint64, disks []DiskStatus) HardwareInfo { // parseRefreshRate extracts the highest refresh rate from system_profiler display output. func parseRefreshRate(output string) string { - lines := strings.Split(output, "\n") maxHz := 0 - for _, line := range lines { + for line := range strings.Lines(output) { lower := strings.ToLower(line) // Look for patterns like "@ 60Hz", "@ 60.00Hz", or "Refresh Rate: 120 Hz". if strings.Contains(lower, "hz") { @@ -100,8 +98,7 @@ func parseRefreshRate(output string) string { } continue } - if strings.HasSuffix(field, "hz") { - numStr := strings.TrimSuffix(field, "hz") + if numStr, ok := strings.CutSuffix(field, "hz"); ok { if numStr == "" && i > 0 { numStr = fields[i-1] } diff --git a/cmd/status/view.go b/cmd/status/view.go index a5ef8ea..a3c590d 100644 --- a/cmd/status/view.go +++ b/cmd/status/view.go @@ -66,10 +66,7 @@ func getMoleFrame(animFrame int, termWidth int) string { body := moleBody[bodyIdx] moleWidth := 15 - maxPos := termWidth - moleWidth - if maxPos < 0 { - maxPos = 0 - } + maxPos := max(termWidth-moleWidth, 0) cycleLength := maxPos * 2 if cycleLength == 0 { @@ -197,10 +194,7 @@ func renderCPUCard(cpu CPUStatus) cardData { } sort.Slice(cores, func(i, j int) bool { return cores[i].val > cores[j].val }) - maxCores := 3 - if len(cores) < maxCores { - maxCores = len(cores) - } + maxCores := min(len(cores), 3) 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)) @@ -356,10 +350,7 @@ func formatDiskLine(label string, d DiskStatus) string { } func ioBar(rate float64) string { - filled := int(rate / 10.0) - if filled > 5 { - filled = 5 - } + filled := min(int(rate/10.0), 5) if filled < 0 { filled = 0 } @@ -391,10 +382,7 @@ func renderProcessCard(procs []ProcessInfo) cardData { } func miniBar(percent float64) string { - filled := int(percent / 20) - if filled > 5 { - filled = 5 - } + filled := min(int(percent/20), 5) if filled < 0 { filled = 0 } @@ -437,10 +425,7 @@ func renderNetworkCard(netStats []NetworkStatus, proxy ProxyStatus) cardData { } func netBar(rate float64) string { - filled := int(rate / 2.0) - if filled > 5 { - filled = 5 - } + filled := min(int(rate/2.0), 5) if filled < 0 { filled = 0 } @@ -551,10 +536,7 @@ func renderSensorsCard(sensors []SensorReading) cardData { func renderCard(data cardData, width int, height int) string { titleText := data.icon + " " + data.title - lineLen := width - lipgloss.Width(titleText) - 2 - if lineLen < 4 { - lineLen = 4 - } + lineLen := max(width-lipgloss.Width(titleText)-2, 4) header := titleStyle.Render(titleText) + " " + lineStyle.Render(strings.Repeat("╌", lineLen)) content := header + "\n" + strings.Join(data.lines, "\n") @@ -576,7 +558,7 @@ func progressBar(percent float64) string { filled := int(percent / 100 * float64(total)) var builder strings.Builder - for i := 0; i < total; i++ { + for i := range total { if i < filled { builder.WriteString("█") } else { @@ -597,7 +579,7 @@ func batteryProgressBar(percent float64) string { filled := int(percent / 100 * float64(total)) var builder strings.Builder - for i := 0; i < total; i++ { + for i := range total { if i < filled { builder.WriteString("█") } else { From f69f53a607e730f0f40e55b2ad004782120abf78 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Tue, 6 Jan 2026 21:03:46 +0800 Subject: [PATCH 3/7] refactor: complete Go modernization with strings.Lines() --- cmd/status/metrics_cpu.go | 5 ++++- cmd/status/metrics_memory.go | 22 +++++++++++----------- cmd/status/metrics_process.go | 6 ++++-- cmd/status/view.go | 5 ++++- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/cmd/status/metrics_cpu.go b/cmd/status/metrics_cpu.go index 59be505..f892bef 100644 --- a/cmd/status/metrics_cpu.go +++ b/cmd/status/metrics_cpu.go @@ -119,7 +119,10 @@ func getCoreTopology() (pCores, eCores int) { return 0, 0 } - lines := strings.Split(strings.TrimSpace(out), "\n") + var lines []string + for line := range strings.Lines(strings.TrimSpace(out)) { + lines = append(lines, line) + } if len(lines) < 4 { return 0, 0 } diff --git a/cmd/status/metrics_memory.go b/cmd/status/metrics_memory.go index 5851dd5..6cdd021 100644 --- a/cmd/status/metrics_memory.go +++ b/cmd/status/metrics_memory.go @@ -46,22 +46,22 @@ func getFileBackedMemory() uint64 { // Parse page size from first line: "Mach Virtual Memory Statistics: (page size of 16384 bytes)" var pageSize uint64 = 4096 // Default - lines := strings.Split(out, "\n") - if len(lines) > 0 { - firstLine := lines[0] - if strings.Contains(firstLine, "page size of") { - if _, after, found := strings.Cut(firstLine, "page size of "); found { - if before, _, found := strings.Cut(after, " bytes"); found { - if size, err := strconv.ParseUint(strings.TrimSpace(before), 10, 64); err == nil { - pageSize = size + firstLine := true + for line := range strings.Lines(out) { + if firstLine { + firstLine = false + if strings.Contains(line, "page size of") { + if _, after, found := strings.Cut(line, "page size of "); found { + if before, _, found := strings.Cut(after, " bytes"); found { + if size, err := strconv.ParseUint(strings.TrimSpace(before), 10, 64); err == nil { + pageSize = size + } } } } } - } - // Parse "File-backed pages: 388975." - for _, line := range lines { + // Parse "File-backed pages: 388975." if strings.Contains(line, "File-backed pages:") { if _, after, found := strings.Cut(line, ":"); found { numStr := strings.TrimSpace(after) diff --git a/cmd/status/metrics_process.go b/cmd/status/metrics_process.go index 5c497a4..b11f25c 100644 --- a/cmd/status/metrics_process.go +++ b/cmd/status/metrics_process.go @@ -21,15 +21,17 @@ func collectTopProcesses() []ProcessInfo { return nil } - lines := strings.Split(strings.TrimSpace(out), "\n") var procs []ProcessInfo - for i, line := range lines { + i := 0 + for line := range strings.Lines(strings.TrimSpace(out)) { if i == 0 { + i++ continue } if i > 5 { break } + i++ fields := strings.Fields(line) if len(fields) < 3 { continue diff --git a/cmd/status/view.go b/cmd/status/view.go index a3c590d..82a4a49 100644 --- a/cmd/status/view.go +++ b/cmd/status/view.go @@ -540,7 +540,10 @@ func renderCard(data cardData, width int, height int) string { header := titleStyle.Render(titleText) + " " + lineStyle.Render(strings.Repeat("╌", lineLen)) content := header + "\n" + strings.Join(data.lines, "\n") - lines := strings.Split(content, "\n") + var lines []string + for line := range strings.Lines(content) { + lines = append(lines, line) + } for len(lines) < height { lines = append(lines, "") } From 0d15177735a86755ce282e14b243f5c756fb422b Mon Sep 17 00:00:00 2001 From: Tw93 Date: Tue, 6 Jan 2026 21:07:51 +0800 Subject: [PATCH 4/7] refactor: remove `trimName` and `renderGPUCard` functions --- cmd/analyze/format.go | 4 ---- cmd/status/view.go | 22 ---------------------- 2 files changed, 26 deletions(-) diff --git a/cmd/analyze/format.go b/cmd/analyze/format.go index 2350879..5ef48d6 100644 --- a/cmd/analyze/format.go +++ b/cmd/analyze/format.go @@ -179,10 +179,6 @@ func calculateNameWidth(termWidth int) int { return available } -func trimName(name string) string { - return trimNameWithWidth(name, 45) // Default width for backward compatibility -} - func trimNameWithWidth(name string, maxWidth int) string { const ( ellipsis = "..." diff --git a/cmd/status/view.go b/cmd/status/view.go index 82a4a49..bdacced 100644 --- a/cmd/status/view.go +++ b/cmd/status/view.go @@ -213,28 +213,6 @@ func renderCPUCard(cpu CPUStatus) cardData { 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 { - if g.Usage >= 0 { - lines = append(lines, fmt.Sprintf("Total %s %5.1f%%", progressBar(g.Usage), g.Usage)) - } - coreInfo := "" - if g.CoreCount > 0 { - coreInfo = fmt.Sprintf(" (%d cores)", g.CoreCount) - } - lines = append(lines, g.Name+coreInfo) - if g.Usage < 0 { - lines = append(lines, subtleStyle.Render("Run with sudo for usage metrics")) - } - } - } - return cardData{icon: iconGPU, title: "GPU", lines: lines} -} - func renderMemoryCard(mem MemoryStatus) cardData { // Check if swap is being used (or at least allocated). hasSwap := mem.SwapTotal > 0 || mem.SwapUsed > 0 From 1fccf6bcf85a06eebe0f69ad2f5ddbf0a0712117 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Thu, 8 Jan 2026 10:16:58 +0800 Subject: [PATCH 5/7] Repair brew update prompts --- lib/manage/update.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/manage/update.sh b/lib/manage/update.sh index 996a16e..8878a4d 100644 --- a/lib/manage/update.sh +++ b/lib/manage/update.sh @@ -96,9 +96,8 @@ ask_for_updates() { fi echo "" - echo -e "${YELLOW}Tip:${NC} Homebrew: brew upgrade / brew upgrade --cask" - echo -e "${YELLOW}Tip:${NC} App Store: open App Store → Updates" - echo -e "${YELLOW}Tip:${NC} macOS: System Settings → General → Software Update" + echo -e "${YELLOW}💡 To update, please run:${NC} ${GREEN}brew upgrade${NC}" + return 1 } From 7d6d5eb8b044871f3530e078cc22b7ec80870c26 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Thu, 8 Jan 2026 10:20:04 +0800 Subject: [PATCH 6/7] Fix the issue with the IDE GoLang cache #269 --- lib/core/base.sh | 1 + lib/manage/update.sh | 2 +- lib/manage/whitelist.sh | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/core/base.sh b/lib/core/base.sh index ab39a66..5a455e9 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -70,6 +70,7 @@ declare -a DEFAULT_WHITELIST_PATTERNS=( "$HOME/Library/Caches/pypoetry/virtualenvs*" "$HOME/Library/Caches/JetBrains*" "$HOME/Library/Caches/com.jetbrains.toolbox*" + "$HOME/Library/Application Support/JetBrains*" "$HOME/Library/Caches/com.apple.finder" "$HOME/Library/Mobile Documents*" # System-critical caches that affect macOS functionality and stability diff --git a/lib/manage/update.sh b/lib/manage/update.sh index 8878a4d..b700f31 100644 --- a/lib/manage/update.sh +++ b/lib/manage/update.sh @@ -96,7 +96,7 @@ ask_for_updates() { fi echo "" - echo -e "${YELLOW}💡 To update, please run:${NC} ${GREEN}brew upgrade${NC}" + echo -e "${YELLOW}💡 Run ${GREEN}brew upgrade${YELLOW} to update${NC}" return 1 } diff --git a/lib/manage/whitelist.sh b/lib/manage/whitelist.sh index 75f0762..e648e9e 100755 --- a/lib/manage/whitelist.sh +++ b/lib/manage/whitelist.sh @@ -85,7 +85,8 @@ Xcode archives (built app packages)|$HOME/Library/Developer/Xcode/Archives/*|ide Xcode internal cache files|$HOME/Library/Caches/com.apple.dt.Xcode/*|ide_cache Xcode iOS device support symbols|$HOME/Library/Developer/Xcode/iOS DeviceSupport/*/Symbols/System/Library/Caches/*|ide_cache Maven local repository (Java dependencies)|$HOME/.m2/repository/*|ide_cache -JetBrains IDEs cache (IntelliJ, PyCharm, WebStorm)|$HOME/Library/Caches/JetBrains/*|ide_cache +JetBrains IDEs data (IntelliJ, PyCharm, WebStorm, GoLand)|$HOME/Library/Application Support/JetBrains/*|ide_cache +JetBrains IDEs cache|$HOME/Library/Caches/JetBrains/*|ide_cache Android Studio cache and indexes|$HOME/Library/Caches/Google/AndroidStudio*/*|ide_cache Android build cache|$HOME/.android/build-cache/*|ide_cache VS Code runtime cache|$HOME/Library/Application Support/Code/Cache/*|ide_cache From 64a580b3a7036d8be066625e1769daded133d383 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Thu, 8 Jan 2026 11:27:47 +0800 Subject: [PATCH 7/7] feat: cat hide toggle and critical fixes (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'k' key to hide/show cat in mo status - Hand-crafted mirror frames for better left-walking animation - Fix extra blank lines bug (strings.Lines → strings.Split) - Fix battery power overflow (ParseInt for negative values) - Optimize README Tips section (8 → 5 items) --- README.md | 13 +++---- cmd/status/main.go | 22 ++++++++++-- cmd/status/metrics_battery.go | 5 +-- cmd/status/view.go | 67 +++++++++++++++++++++++++++++------ 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 55276ca..72f1170 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,10 @@ mo purge --paths # Configure project scan directories ## Tips - **Terminal**: iTerm2 has known compatibility issues; we recommend Alacritty, kitty, WezTerm, Ghostty, or Warp. -- **Safety**: Built with strict protections. See our [Security Audit](SECURITY_AUDIT.md). Preview changes with `mo clean --dry-run`. -- **Whitelist**: Manage protected paths with `mo clean --whitelist`. -- **Touch ID**: Enable Touch ID for sudo commands by running `mo touchid`. -- **Shell Completion**: Enable tab completion by running `mo completion` (auto-detect and install). -- **Navigation**: Supports standard arrow keys and Vim bindings (`h/j/k/l`). -- **Debug**: View detailed logs by appending the `--debug` flag (e.g., `mo clean --debug`). -- **Detailed Preview**: Combine `--dry-run --debug` for comprehensive operation details including risk levels, file paths, sizes, and expected outcomes. Check `~/.config/mole/mole_debug_session.log` for full details. +- **Safety**: Built with strict protections. See [Security Audit](SECURITY_AUDIT.md). Preview changes with `mo clean --dry-run`. +- **Debug Mode**: Use `--debug` for detailed logs (e.g., `mo clean --debug`). Combine with `--dry-run` for comprehensive preview including risk levels and file details. +- **Navigation**: Supports arrow keys and Vim bindings (`h/j/k/l`). +- **Configuration**: Run `mo touchid` for Touch ID sudo, `mo completion` for shell tab completion, `mo clean --whitelist` to manage protected paths. ## Features in Detail @@ -188,7 +185,7 @@ Up ▮▯▯▯▯ 0.8 MB/s Chrome ▮▮▮▯▯ 2 Proxy HTTP · 192.168.1.100 Terminal ▮▯▯▯▯ 12.5% ``` -Health score based on CPU, memory, disk, temperature, and I/O load. Color-coded by range. +Health score based on CPU, memory, disk, temperature, and I/O load. Color-coded by range. Press `k` to hide/show cat, `q` to quit. ### Project Artifact Purge diff --git a/cmd/status/main.go b/cmd/status/main.go index d1bc5a5..cc5ba0e 100644 --- a/cmd/status/main.go +++ b/cmd/status/main.go @@ -34,11 +34,13 @@ type model struct { lastUpdated time.Time collecting bool animFrame int + catHidden bool // true = hidden, false = visible } func newModel() model { return model{ collector: NewCollector(), + catHidden: false, } } @@ -52,6 +54,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "q", "esc", "ctrl+c": return m, tea.Quit + case "k": + // Toggle cat visibility + m.catHidden = !m.catHidden + return m, nil } case tea.WindowSizeMsg: m.width = msg.Width @@ -89,7 +95,7 @@ func (m model) View() string { return "Loading..." } - header := renderHeader(m.metrics, m.errMessage, m.animFrame, m.width) + header := renderHeader(m.metrics, m.errMessage, m.animFrame, m.width, m.catHidden) cardWidth := 0 if m.width > 80 { cardWidth = maxInt(24, m.width/2-4) @@ -104,10 +110,20 @@ func (m model) View() string { } rendered = append(rendered, renderCard(c, cardWidth, 0)) } - return header + "\n" + lipgloss.JoinVertical(lipgloss.Left, rendered...) + result := header + "\n" + lipgloss.JoinVertical(lipgloss.Left, rendered...) + // Add extra newline if cat is hidden for better spacing + if m.catHidden { + result = header + "\n\n" + lipgloss.JoinVertical(lipgloss.Left, rendered...) + } + return result } - return header + "\n" + renderTwoColumns(cards, m.width) + twoCol := renderTwoColumns(cards, m.width) + // Add extra newline if cat is hidden for better spacing + if m.catHidden { + return header + "\n\n" + twoCol + } + return header + "\n" + twoCol } func (m model) collectCmd() tea.Cmd { diff --git a/cmd/status/metrics_battery.go b/cmd/status/metrics_battery.go index ef3515d..57f1f8b 100644 --- a/cmd/status/metrics_battery.go +++ b/cmd/status/metrics_battery.go @@ -238,8 +238,9 @@ func collectThermal() ThermalStatus { 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 + // Parse as int64 first to handle negative values (charging) + if powerMW, err := strconv.ParseInt(valStr, 10, 64); err == nil { + thermal.BatteryPower = float64(powerMW) / 1000.0 } } } diff --git a/cmd/status/view.go b/cmd/status/view.go index bdacced..2691452 100644 --- a/cmd/status/view.go +++ b/cmd/status/view.go @@ -32,7 +32,7 @@ const ( iconProcs = "❊" ) -// Mole body frames. +// Mole body frames (facing right). var moleBody = [][]string{ { ` /\_/\`, @@ -60,11 +60,36 @@ var moleBody = [][]string{ }, } +// Mirror mole body frames (facing left). +var moleBodyMirror = [][]string{ + { + ` /\_/\`, + ` / o o \___`, + ` \ =-= ___\`, + ` (m-m-(____/`, + }, + { + ` /\_/\`, + ` / o o \___`, + ` \ =-= ___\`, + ` (__mm(____/`, + }, + { + ` /\_/\`, + ` / · · \___`, + ` \ =-= ___\`, + ` (m__m-(___/`, + }, + { + ` /\_/\`, + ` / o o \___`, + ` \ =-= ___\`, + ` (-mm-(____/`, + }, +} + // getMoleFrame renders the animated mole. func getMoleFrame(animFrame int, termWidth int) string { - bodyIdx := animFrame % len(moleBody) - body := moleBody[bodyIdx] - moleWidth := 15 maxPos := max(termWidth-moleWidth, 0) @@ -73,10 +98,22 @@ func getMoleFrame(animFrame int, termWidth int) string { cycleLength = 1 } pos := animFrame % cycleLength - if pos > maxPos { + movingLeft := pos > maxPos + if movingLeft { pos = cycleLength - pos } + // Use mirror frames when moving left + var frames [][]string + if movingLeft { + frames = moleBodyMirror + } else { + frames = moleBody + } + + bodyIdx := animFrame % len(frames) + body := frames[bodyIdx] + padding := strings.Repeat(" ", pos) var lines []string @@ -93,7 +130,7 @@ type cardData struct { lines []string } -func renderHeader(m MetricsSnapshot, errMsg string, animFrame int, termWidth int) string { +func renderHeader(m MetricsSnapshot, errMsg string, animFrame int, termWidth int, catHidden bool) string { title := titleStyle.Render("Mole Status") scoreStyle := getScoreStyle(m.HealthScore) @@ -131,11 +168,21 @@ func renderHeader(m MetricsSnapshot, errMsg string, animFrame int, termWidth int headerLine := title + " " + scoreText + " " + strings.Join(infoParts, " · ") - mole := getMoleFrame(animFrame, termWidth) + // Show cat unless hidden + var mole string + if !catHidden { + mole = getMoleFrame(animFrame, termWidth) + } if errMsg != "" { + if mole == "" { + return lipgloss.JoinVertical(lipgloss.Left, headerLine, "", dangerStyle.Render("ERROR: "+errMsg), "") + } return lipgloss.JoinVertical(lipgloss.Left, headerLine, "", mole, dangerStyle.Render("ERROR: "+errMsg), "") } + if mole == "" { + return headerLine + } return headerLine + "\n" + mole } @@ -464,6 +511,7 @@ func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData { statusText += fmt.Sprintf(" · %.0fW Adapter", thermal.AdapterPower) } } else if thermal.BatteryPower > 0 { + // Only show battery power when discharging (positive value) statusText += fmt.Sprintf(" · %.0fW", thermal.BatteryPower) } lines = append(lines, statusStyle.Render(statusText+statusIcon)) @@ -518,10 +566,7 @@ func renderCard(data cardData, width int, height int) string { header := titleStyle.Render(titleText) + " " + lineStyle.Render(strings.Repeat("╌", lineLen)) content := header + "\n" + strings.Join(data.lines, "\n") - var lines []string - for line := range strings.Lines(content) { - lines = append(lines, line) - } + lines := strings.Split(content, "\n") for len(lines) < height { lines = append(lines, "") }