1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 15:39:42 +00:00

feat: cat hide toggle and critical fixes (#272)

- 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)
This commit is contained in:
Tw93
2026-01-08 11:27:47 +08:00
parent 7d6d5eb8b0
commit 64a580b3a7
4 changed files with 83 additions and 24 deletions

View File

@@ -71,13 +71,10 @@ mo purge --paths # Configure project scan directories
## Tips ## Tips
- **Terminal**: iTerm2 has known compatibility issues; we recommend Alacritty, kitty, WezTerm, Ghostty, or Warp. - **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`. - **Safety**: Built with strict protections. See [Security Audit](SECURITY_AUDIT.md). Preview changes with `mo clean --dry-run`.
- **Whitelist**: Manage protected paths with `mo clean --whitelist`. - **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.
- **Touch ID**: Enable Touch ID for sudo commands by running `mo touchid`. - **Navigation**: Supports arrow keys and Vim bindings (`h/j/k/l`).
- **Shell Completion**: Enable tab completion by running `mo completion` (auto-detect and install). - **Configuration**: Run `mo touchid` for Touch ID sudo, `mo completion` for shell tab completion, `mo clean --whitelist` to manage protected paths.
- **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.
## Features in Detail ## Features in Detail
@@ -188,7 +185,7 @@ Up ▮▯▯▯▯ 0.8 MB/s Chrome ▮▮▮▯▯ 2
Proxy HTTP · 192.168.1.100 Terminal ▮▯▯▯▯ 12.5% 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 ### Project Artifact Purge

View File

@@ -34,11 +34,13 @@ type model struct {
lastUpdated time.Time lastUpdated time.Time
collecting bool collecting bool
animFrame int animFrame int
catHidden bool // true = hidden, false = visible
} }
func newModel() model { func newModel() model {
return model{ return model{
collector: NewCollector(), collector: NewCollector(),
catHidden: false,
} }
} }
@@ -52,6 +54,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() { switch msg.String() {
case "q", "esc", "ctrl+c": case "q", "esc", "ctrl+c":
return m, tea.Quit return m, tea.Quit
case "k":
// Toggle cat visibility
m.catHidden = !m.catHidden
return m, nil
} }
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.width = msg.Width m.width = msg.Width
@@ -89,7 +95,7 @@ func (m model) View() string {
return "Loading..." 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 cardWidth := 0
if m.width > 80 { if m.width > 80 {
cardWidth = maxInt(24, m.width/2-4) cardWidth = maxInt(24, m.width/2-4)
@@ -104,10 +110,20 @@ func (m model) View() string {
} }
rendered = append(rendered, renderCard(c, cardWidth, 0)) 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 { func (m model) collectCmd() tea.Cmd {

View File

@@ -238,8 +238,9 @@ func collectThermal() ThermalStatus {
valStr, _, _ = strings.Cut(valStr, ",") valStr, _, _ = strings.Cut(valStr, ",")
valStr, _, _ = strings.Cut(valStr, "}") valStr, _, _ = strings.Cut(valStr, "}")
valStr = strings.TrimSpace(valStr) valStr = strings.TrimSpace(valStr)
if powerMW, err := strconv.ParseFloat(valStr, 64); err == nil { // Parse as int64 first to handle negative values (charging)
thermal.BatteryPower = powerMW / 1000.0 if powerMW, err := strconv.ParseInt(valStr, 10, 64); err == nil {
thermal.BatteryPower = float64(powerMW) / 1000.0
} }
} }
} }

View File

@@ -32,7 +32,7 @@ const (
iconProcs = "❊" iconProcs = "❊"
) )
// Mole body frames. // Mole body frames (facing right).
var moleBody = [][]string{ 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. // getMoleFrame renders the animated mole.
func getMoleFrame(animFrame int, termWidth int) string { func getMoleFrame(animFrame int, termWidth int) string {
bodyIdx := animFrame % len(moleBody)
body := moleBody[bodyIdx]
moleWidth := 15 moleWidth := 15
maxPos := max(termWidth-moleWidth, 0) maxPos := max(termWidth-moleWidth, 0)
@@ -73,10 +98,22 @@ func getMoleFrame(animFrame int, termWidth int) string {
cycleLength = 1 cycleLength = 1
} }
pos := animFrame % cycleLength pos := animFrame % cycleLength
if pos > maxPos { movingLeft := pos > maxPos
if movingLeft {
pos = cycleLength - pos 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) padding := strings.Repeat(" ", pos)
var lines []string var lines []string
@@ -93,7 +130,7 @@ type cardData struct {
lines []string 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") title := titleStyle.Render("Mole Status")
scoreStyle := getScoreStyle(m.HealthScore) scoreStyle := getScoreStyle(m.HealthScore)
@@ -131,11 +168,21 @@ func renderHeader(m MetricsSnapshot, errMsg string, animFrame int, termWidth int
headerLine := title + " " + scoreText + " " + strings.Join(infoParts, " · ") 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 errMsg != "" {
if mole == "" {
return lipgloss.JoinVertical(lipgloss.Left, headerLine, "", dangerStyle.Render("ERROR: "+errMsg), "")
}
return lipgloss.JoinVertical(lipgloss.Left, headerLine, "", mole, dangerStyle.Render("ERROR: "+errMsg), "") return lipgloss.JoinVertical(lipgloss.Left, headerLine, "", mole, dangerStyle.Render("ERROR: "+errMsg), "")
} }
if mole == "" {
return headerLine
}
return headerLine + "\n" + mole return headerLine + "\n" + mole
} }
@@ -464,6 +511,7 @@ func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData {
statusText += fmt.Sprintf(" · %.0fW Adapter", thermal.AdapterPower) statusText += fmt.Sprintf(" · %.0fW Adapter", thermal.AdapterPower)
} }
} else if thermal.BatteryPower > 0 { } else if thermal.BatteryPower > 0 {
// Only show battery power when discharging (positive value)
statusText += fmt.Sprintf(" · %.0fW", thermal.BatteryPower) statusText += fmt.Sprintf(" · %.0fW", thermal.BatteryPower)
} }
lines = append(lines, statusStyle.Render(statusText+statusIcon)) 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)) header := titleStyle.Render(titleText) + " " + lineStyle.Render(strings.Repeat("╌", lineLen))
content := header + "\n" + strings.Join(data.lines, "\n") content := header + "\n" + strings.Join(data.lines, "\n")
var lines []string lines := strings.Split(content, "\n")
for line := range strings.Lines(content) {
lines = append(lines, line)
}
for len(lines) < height { for len(lines) < height {
lines = append(lines, "") lines = append(lines, "")
} }