mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 14:26:46 +00:00
Merge branch 'main' into dev
This commit is contained in:
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Test
|
||||
name: Unit & Integration Tests
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4
|
||||
|
||||
@@ -83,7 +83,8 @@ type MemoryStatus struct {
|
||||
UsedPercent float64
|
||||
SwapUsed uint64
|
||||
SwapTotal uint64
|
||||
Pressure string // macOS memory pressure: normal/warn/critical
|
||||
Cached uint64 // File cache that can be freed if needed
|
||||
Pressure string // macOS memory pressure: normal/warn/critical
|
||||
}
|
||||
|
||||
type DiskStatus struct {
|
||||
@@ -115,6 +116,7 @@ type BatteryStatus struct {
|
||||
TimeLeft string
|
||||
Health string
|
||||
CycleCount int
|
||||
Capacity int // Maximum capacity percentage (e.g., 85 means 85% of original)
|
||||
}
|
||||
|
||||
type ThermalStatus struct {
|
||||
|
||||
@@ -32,9 +32,9 @@ func collectBatteries() (batts []BatteryStatus, err error) {
|
||||
// macOS: pmset for real-time percentage/status.
|
||||
if runtime.GOOS == "darwin" && commandExists("pmset") {
|
||||
if out, err := runCmd(context.Background(), "pmset", "-g", "batt"); err == nil {
|
||||
// Health/cycles from cached system_profiler.
|
||||
health, cycles := getCachedPowerData()
|
||||
if batts := parsePMSet(out, health, cycles); len(batts) > 0 {
|
||||
// Health/cycles/capacity from cached system_profiler.
|
||||
health, cycles, capacity := getCachedPowerData()
|
||||
if batts := parsePMSet(out, health, cycles, capacity); len(batts) > 0 {
|
||||
return batts, nil
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func collectBatteries() (batts []BatteryStatus, err error) {
|
||||
return nil, errors.New("no battery data found")
|
||||
}
|
||||
|
||||
func parsePMSet(raw string, health string, cycles int) []BatteryStatus {
|
||||
func parsePMSet(raw string, health string, cycles int, capacity int) []BatteryStatus {
|
||||
lines := strings.Split(raw, "\n")
|
||||
var out []BatteryStatus
|
||||
var timeLeft string
|
||||
@@ -115,16 +115,17 @@ func parsePMSet(raw string, health string, cycles int) []BatteryStatus {
|
||||
TimeLeft: timeLeft,
|
||||
Health: health,
|
||||
CycleCount: cycles,
|
||||
Capacity: capacity,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// getCachedPowerData returns condition and cycles from cached system_profiler.
|
||||
func getCachedPowerData() (health string, cycles int) {
|
||||
// getCachedPowerData returns condition, cycles, and capacity from cached system_profiler.
|
||||
func getCachedPowerData() (health string, cycles int, capacity int) {
|
||||
out := getSystemPowerOutput()
|
||||
if out == "" {
|
||||
return "", 0
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
lines := strings.Split(out, "\n")
|
||||
@@ -140,8 +141,15 @@ func getCachedPowerData() (health string, cycles int) {
|
||||
health = strings.TrimSpace(after)
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "maximum capacity") {
|
||||
if _, after, found := strings.Cut(line, ":"); found {
|
||||
capacityStr := strings.TrimSpace(after)
|
||||
capacityStr = strings.TrimSuffix(capacityStr, "%")
|
||||
capacity, _ = strconv.Atoi(strings.TrimSpace(capacityStr))
|
||||
}
|
||||
}
|
||||
}
|
||||
return health, cycles
|
||||
return health, cycles, capacity
|
||||
}
|
||||
|
||||
func getSystemPowerOutput() string {
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,16 +19,62 @@ func collectMemory() (MemoryStatus, error) {
|
||||
swap, _ := mem.SwapMemory()
|
||||
pressure := getMemoryPressure()
|
||||
|
||||
// On macOS, vm.Cached is 0, so we calculate from file-backed pages.
|
||||
cached := vm.Cached
|
||||
if runtime.GOOS == "darwin" && cached == 0 {
|
||||
cached = getFileBackedMemory()
|
||||
}
|
||||
|
||||
return MemoryStatus{
|
||||
Used: vm.Used,
|
||||
Total: vm.Total,
|
||||
UsedPercent: vm.UsedPercent,
|
||||
SwapUsed: swap.Used,
|
||||
SwapTotal: swap.Total,
|
||||
Cached: cached,
|
||||
Pressure: pressure,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getFileBackedMemory() uint64 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
out, err := runCmd(ctx, "vm_stat")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse "File-backed pages: 388975."
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "File-backed pages:") {
|
||||
if _, after, found := strings.Cut(line, ":"); found {
|
||||
numStr := strings.TrimSpace(after)
|
||||
numStr = strings.TrimSuffix(numStr, ".")
|
||||
if pages, err := strconv.ParseUint(numStr, 10, 64); err == nil {
|
||||
return pages * pageSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getMemoryPressure() string {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return ""
|
||||
|
||||
@@ -5,19 +5,18 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#C79FD7")).Bold(true)
|
||||
subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#737373"))
|
||||
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD75F"))
|
||||
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F5F")).Bold(true)
|
||||
okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#A5D6A7"))
|
||||
lineStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#404040"))
|
||||
hatStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF4D4D"))
|
||||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#C79FD7")).Bold(true)
|
||||
subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#737373"))
|
||||
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD75F"))
|
||||
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F5F")).Bold(true)
|
||||
okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#A5D6A7"))
|
||||
lineStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#404040"))
|
||||
|
||||
primaryStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#BD93F9"))
|
||||
)
|
||||
|
||||
@@ -33,14 +32,6 @@ const (
|
||||
iconProcs = "❊"
|
||||
)
|
||||
|
||||
// isChristmasSeason reports Dec 10-31.
|
||||
func isChristmasSeason() bool {
|
||||
now := time.Now()
|
||||
month := now.Month()
|
||||
day := now.Day()
|
||||
return month == time.December && day >= 10 && day <= 31
|
||||
}
|
||||
|
||||
// Mole body frames.
|
||||
var moleBody = [][]string{
|
||||
{
|
||||
@@ -69,55 +60,10 @@ var moleBody = [][]string{
|
||||
},
|
||||
}
|
||||
|
||||
// Mole body frames with Christmas hat.
|
||||
var moleBodyWithHat = [][]string{
|
||||
{
|
||||
` *`,
|
||||
` /o\`,
|
||||
` {/\_/\}`,
|
||||
` ___/ o o \`,
|
||||
` /___ =-= /`,
|
||||
` \____)-m-m)`,
|
||||
},
|
||||
{
|
||||
` *`,
|
||||
` /o\`,
|
||||
` {/\_/\}`,
|
||||
` ___/ o o \`,
|
||||
` /___ =-= /`,
|
||||
` \____)mm__)`,
|
||||
},
|
||||
{
|
||||
` *`,
|
||||
` /o\`,
|
||||
` {/\_/\}`,
|
||||
` ___/ · · \`,
|
||||
` /___ =-= /`,
|
||||
` \___)-m__m)`,
|
||||
},
|
||||
{
|
||||
` *`,
|
||||
` /o\`,
|
||||
` {/\_/\}`,
|
||||
` ___/ o o \`,
|
||||
` /___ =-= /`,
|
||||
` \____)-mm-)`,
|
||||
},
|
||||
}
|
||||
|
||||
// getMoleFrame renders the animated mole.
|
||||
func getMoleFrame(animFrame int, termWidth int) string {
|
||||
var body []string
|
||||
var bodyIdx int
|
||||
isChristmas := isChristmasSeason()
|
||||
|
||||
if isChristmas {
|
||||
bodyIdx = animFrame % len(moleBodyWithHat)
|
||||
body = moleBodyWithHat[bodyIdx]
|
||||
} else {
|
||||
bodyIdx = animFrame % len(moleBody)
|
||||
body = moleBody[bodyIdx]
|
||||
}
|
||||
bodyIdx := animFrame % len(moleBody)
|
||||
body := moleBody[bodyIdx]
|
||||
|
||||
moleWidth := 15
|
||||
maxPos := termWidth - moleWidth
|
||||
@@ -137,18 +83,8 @@ func getMoleFrame(animFrame int, termWidth int) string {
|
||||
padding := strings.Repeat(" ", pos)
|
||||
var lines []string
|
||||
|
||||
if isChristmas {
|
||||
for i, line := range body {
|
||||
if i < 3 {
|
||||
lines = append(lines, padding+hatStyle.Render(line))
|
||||
} else {
|
||||
lines = append(lines, padding+line)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, line := range body {
|
||||
lines = append(lines, padding+line)
|
||||
}
|
||||
for _, line := range body {
|
||||
lines = append(lines, padding+line)
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
@@ -193,7 +129,7 @@ func renderHeader(m MetricsSnapshot, errMsg string, animFrame int, termWidth int
|
||||
infoParts = append(infoParts, m.Hardware.OSVersion)
|
||||
}
|
||||
|
||||
headerLine := title + " " + scoreText + " " + subtleStyle.Render(strings.Join(infoParts, " · "))
|
||||
headerLine := title + " " + scoreText + " " + strings.Join(infoParts, " · ")
|
||||
|
||||
mole := getMoleFrame(animFrame, termWidth)
|
||||
|
||||
@@ -245,14 +181,6 @@ func renderCPUCard(cpu CPUStatus) cardData {
|
||||
var lines []string
|
||||
lines = append(lines, fmt.Sprintf("Total %s %5.1f%%", progressBar(cpu.Usage), cpu.Usage))
|
||||
|
||||
if cpu.PCoreCount > 0 && cpu.ECoreCount > 0 {
|
||||
lines = append(lines, subtleStyle.Render(fmt.Sprintf("Load %.2f / %.2f / %.2f (%dP+%dE)",
|
||||
cpu.Load1, cpu.Load5, cpu.Load15, cpu.PCoreCount, cpu.ECoreCount)))
|
||||
} else {
|
||||
lines = append(lines, subtleStyle.Render(fmt.Sprintf("%.2f / %.2f / %.2f (%d cores)",
|
||||
cpu.Load1, cpu.Load5, cpu.Load15, cpu.LogicalCPU)))
|
||||
}
|
||||
|
||||
if cpu.PerCoreEstimated {
|
||||
lines = append(lines, subtleStyle.Render("Per-core data unavailable (using averaged load)"))
|
||||
} else if len(cpu.PerCore) > 0 {
|
||||
@@ -276,6 +204,15 @@ func renderCPUCard(cpu CPUStatus) cardData {
|
||||
}
|
||||
}
|
||||
|
||||
// Load line at the end
|
||||
if cpu.PCoreCount > 0 && cpu.ECoreCount > 0 {
|
||||
lines = append(lines, fmt.Sprintf("Load %.2f / %.2f / %.2f (%dP+%dE)",
|
||||
cpu.Load1, cpu.Load5, cpu.Load15, cpu.PCoreCount, cpu.ECoreCount))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("Load %.2f / %.2f / %.2f (%d cores)",
|
||||
cpu.Load1, cpu.Load5, cpu.Load15, cpu.LogicalCPU))
|
||||
}
|
||||
|
||||
return cardData{icon: iconCPU, title: "CPU", lines: lines}
|
||||
}
|
||||
|
||||
@@ -290,9 +227,9 @@ func renderGPUCard(gpus []GPUStatus) cardData {
|
||||
}
|
||||
coreInfo := ""
|
||||
if g.CoreCount > 0 {
|
||||
coreInfo = fmt.Sprintf(" (%d cores)", g.CoreCount)
|
||||
coreInfo = fmt.Sprintf(" (%d cores)", g.CoreCount)
|
||||
}
|
||||
lines = append(lines, subtleStyle.Render(g.Name+coreInfo))
|
||||
lines = append(lines, g.Name+coreInfo)
|
||||
if g.Usage < 0 {
|
||||
lines = append(lines, subtleStyle.Render("Run with sudo for usage metrics"))
|
||||
}
|
||||
@@ -302,22 +239,48 @@ func renderGPUCard(gpus []GPUStatus) cardData {
|
||||
}
|
||||
|
||||
func renderMemoryCard(mem MemoryStatus) cardData {
|
||||
// Check if swap is being used (or at least allocated).
|
||||
hasSwap := mem.SwapTotal > 0 || mem.SwapUsed > 0
|
||||
|
||||
var lines []string
|
||||
// Line 1: Used
|
||||
lines = append(lines, fmt.Sprintf("Used %s %5.1f%%", progressBar(mem.UsedPercent), mem.UsedPercent))
|
||||
lines = append(lines, subtleStyle.Render(fmt.Sprintf("%s / %s total", humanBytes(mem.Used), humanBytes(mem.Total))))
|
||||
available := mem.Total - mem.Used
|
||||
|
||||
// Line 2: Free
|
||||
freePercent := 100 - mem.UsedPercent
|
||||
lines = append(lines, fmt.Sprintf("Free %s %5.1f%%", progressBar(freePercent), freePercent))
|
||||
lines = append(lines, subtleStyle.Render(fmt.Sprintf("%s available", humanBytes(available))))
|
||||
if mem.SwapTotal > 0 || mem.SwapUsed > 0 {
|
||||
|
||||
if hasSwap {
|
||||
// Layout with Swap:
|
||||
// 3. Swap (progress bar + text)
|
||||
// 4. Total
|
||||
// 5. Avail
|
||||
var swapPercent float64
|
||||
if mem.SwapTotal > 0 {
|
||||
swapPercent = (float64(mem.SwapUsed) / float64(mem.SwapTotal)) * 100.0
|
||||
}
|
||||
swapText := subtleStyle.Render(fmt.Sprintf("(%s/%s)", humanBytesCompact(mem.SwapUsed), humanBytesCompact(mem.SwapTotal)))
|
||||
swapText := fmt.Sprintf("(%s/%s)", humanBytesCompact(mem.SwapUsed), humanBytesCompact(mem.SwapTotal))
|
||||
lines = append(lines, fmt.Sprintf("Swap %s %5.1f%% %s", progressBar(swapPercent), swapPercent, swapText))
|
||||
|
||||
lines = append(lines, fmt.Sprintf("Total %s / %s", humanBytes(mem.Used), humanBytes(mem.Total)))
|
||||
lines = append(lines, fmt.Sprintf("Avail %s", humanBytes(mem.Total-mem.Used))) // Simplified avail logic for consistency
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("Swap %s", subtleStyle.Render("not in use")))
|
||||
// Layout without Swap:
|
||||
// 3. Total
|
||||
// 4. Cached (if > 0)
|
||||
// 5. Avail
|
||||
lines = append(lines, fmt.Sprintf("Total %s / %s", humanBytes(mem.Used), humanBytes(mem.Total)))
|
||||
|
||||
if mem.Cached > 0 {
|
||||
lines = append(lines, fmt.Sprintf("Cached %s", humanBytes(mem.Cached)))
|
||||
}
|
||||
// Calculate available if not provided directly, or use Total-Used as proxy if needed,
|
||||
// but typically available is more nuanced. Using what we have.
|
||||
// Re-calculating available based on logic if needed, but mem.Total - mem.Used is often "Avail"
|
||||
// in simple terms for this view or we could use the passed definition.
|
||||
// Original code calculated: available := mem.Total - mem.Used
|
||||
available := mem.Total - mem.Used
|
||||
lines = append(lines, fmt.Sprintf("Avail %s", humanBytes(available)))
|
||||
}
|
||||
// Memory pressure status.
|
||||
if mem.Pressure != "" {
|
||||
@@ -464,7 +427,7 @@ func renderNetworkCard(netStats []NetworkStatus, proxy ProxyStatus) cardData {
|
||||
infoParts = append(infoParts, primaryIP)
|
||||
}
|
||||
if len(infoParts) > 0 {
|
||||
lines = append(lines, subtleStyle.Render(strings.Join(infoParts, " · ")))
|
||||
lines = append(lines, strings.Join(infoParts, " · "))
|
||||
}
|
||||
}
|
||||
return cardData{icon: iconNetwork, title: "Network", lines: lines}
|
||||
@@ -501,6 +464,17 @@ func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData {
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("Level %s %s", batteryProgressBar(b.Percent), percentText))
|
||||
|
||||
// Add capacity line if available.
|
||||
if b.Capacity > 0 {
|
||||
capacityText := fmt.Sprintf("%5d%%", b.Capacity)
|
||||
if b.Capacity < 70 {
|
||||
capacityText = dangerStyle.Render(capacityText)
|
||||
} else if b.Capacity < 85 {
|
||||
capacityText = warnStyle.Render(capacityText)
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("Health %s %s", batteryProgressBar(float64(b.Capacity)), capacityText))
|
||||
}
|
||||
|
||||
statusIcon := ""
|
||||
statusStyle := subtleStyle
|
||||
if statusLower == "charging" || statusLower == "charged" {
|
||||
@@ -537,13 +511,13 @@ func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData {
|
||||
}
|
||||
|
||||
if thermal.CPUTemp > 0 {
|
||||
tempStyle := subtleStyle
|
||||
tempText := fmt.Sprintf("%.0f°C", thermal.CPUTemp)
|
||||
if thermal.CPUTemp > 80 {
|
||||
tempStyle = dangerStyle
|
||||
tempText = dangerStyle.Render(tempText)
|
||||
} else if thermal.CPUTemp > 60 {
|
||||
tempStyle = warnStyle
|
||||
tempText = warnStyle.Render(tempText)
|
||||
}
|
||||
healthParts = append(healthParts, tempStyle.Render(fmt.Sprintf("%.0f°C", thermal.CPUTemp)))
|
||||
healthParts = append(healthParts, tempText)
|
||||
}
|
||||
|
||||
if thermal.FanSpeed > 0 {
|
||||
@@ -551,7 +525,7 @@ func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData {
|
||||
}
|
||||
|
||||
if len(healthParts) > 0 {
|
||||
lines = append(lines, subtleStyle.Render(strings.Join(healthParts, " · ")))
|
||||
lines = append(lines, strings.Join(healthParts, " · "))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,21 +55,6 @@ readonly MOLE_TM_BACKUP_SAFE_HOURS=48 # TM backup safety window (hours)
|
||||
readonly MOLE_MAX_DS_STORE_FILES=500 # Max .DS_Store files to clean per scan
|
||||
readonly MOLE_MAX_ORPHAN_ITERATIONS=100 # Max iterations for orphaned app data scan
|
||||
|
||||
# ============================================================================
|
||||
# Seasonal Functions
|
||||
# ============================================================================
|
||||
is_christmas_season() {
|
||||
local month day
|
||||
month=$(date +%-m)
|
||||
day=$(date +%-d)
|
||||
|
||||
# December 10 to December 31
|
||||
if [[ $month -eq 12 && $day -ge 10 && $day -le 31 ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Whitelist Configuration
|
||||
# ============================================================================
|
||||
|
||||
58
mole
58
mole
@@ -13,7 +13,7 @@ source "$SCRIPT_DIR/lib/core/commands.sh"
|
||||
trap cleanup_temp_files EXIT INT TERM
|
||||
|
||||
# Version and update helpers
|
||||
VERSION="1.17.0"
|
||||
VERSION="1.18.0"
|
||||
MOLE_TAGLINE="Deep clean and optimize your Mac."
|
||||
|
||||
is_touchid_configured() {
|
||||
@@ -129,24 +129,9 @@ animate_mole_intro() {
|
||||
|
||||
local -a mole_lines=()
|
||||
|
||||
if is_christmas_season; then
|
||||
while IFS= read -r line; do
|
||||
mole_lines+=("$line")
|
||||
done << 'EOF'
|
||||
*
|
||||
/o\
|
||||
{/\_/\}
|
||||
____/ o o \
|
||||
/~____ =o= /
|
||||
(______)__m_m)
|
||||
/ \
|
||||
__/ /\ \__
|
||||
/__/ \__\_
|
||||
EOF
|
||||
else
|
||||
while IFS= read -r line; do
|
||||
mole_lines+=("$line")
|
||||
done << 'EOF'
|
||||
while IFS= read -r line; do
|
||||
mole_lines+=("$line")
|
||||
done << 'EOF'
|
||||
/\_/\
|
||||
____/ o o \
|
||||
/~____ =o= /
|
||||
@@ -155,37 +140,20 @@ EOF
|
||||
__/ /\ \__
|
||||
/__/ \__\_
|
||||
EOF
|
||||
fi
|
||||
|
||||
local idx
|
||||
local hat_color="${RED}"
|
||||
local body_cutoff
|
||||
local body_cutoff=4
|
||||
local body_color="${PURPLE}"
|
||||
local ground_color="${GREEN}"
|
||||
|
||||
if is_christmas_season; then
|
||||
body_cutoff=6
|
||||
for idx in "${!mole_lines[@]}"; do
|
||||
if ((idx < 3)); then
|
||||
printf "%s\n" "${hat_color}${mole_lines[$idx]}${NC}"
|
||||
elif ((idx < body_cutoff)); then
|
||||
printf "%s\n" "${body_color}${mole_lines[$idx]}${NC}"
|
||||
else
|
||||
printf "%s\n" "${ground_color}${mole_lines[$idx]}${NC}"
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
else
|
||||
body_cutoff=4
|
||||
for idx in "${!mole_lines[@]}"; do
|
||||
if ((idx < body_cutoff)); then
|
||||
printf "%s\n" "${body_color}${mole_lines[$idx]}${NC}"
|
||||
else
|
||||
printf "%s\n" "${ground_color}${mole_lines[$idx]}${NC}"
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
fi
|
||||
for idx in "${!mole_lines[@]}"; do
|
||||
if ((idx < body_cutoff)); then
|
||||
printf "%s\n" "${body_color}${mole_lines[$idx]}${NC}"
|
||||
else
|
||||
printf "%s\n" "${ground_color}${mole_lines[$idx]}${NC}"
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
printf '\n'
|
||||
sleep 0.5
|
||||
|
||||
Reference in New Issue
Block a user