From 64a580b3a7036d8be066625e1769daded133d383 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Thu, 8 Jan 2026 11:27:47 +0800 Subject: [PATCH] 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, "") }