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:
13
README.md
13
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, "")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user