From 00a712711ecf82d02e802a2148db115184961bd3 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 14 Jan 2026 03:59:21 +0000 Subject: [PATCH 01/11] chore: auto format code --- bin/purge.sh | 4 ++-- cmd/status/metrics_health_test.go | 16 ++++++++-------- lib/core/app_protection.sh | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bin/purge.sh b/bin/purge.sh index 4f7b104..b169a77 100755 --- a/bin/purge.sh +++ b/bin/purge.sh @@ -99,11 +99,11 @@ perform_purge() { truncate_path() { local path="$1" local term_cols - term_cols=$(tput cols 2>/dev/null || echo 80) + term_cols=$(tput cols 2> /dev/null || echo 80) # Reserve some space for the spinner and text (approx 20 chars) local max_len=$((term_cols - 20)) # Ensure a reasonable minimum width - if (( max_len < 40 )); then + if ((max_len < 40)); then max_len=40 fi diff --git a/cmd/status/metrics_health_test.go b/cmd/status/metrics_health_test.go index 8c4362f..3bd2497 100644 --- a/cmd/status/metrics_health_test.go +++ b/cmd/status/metrics_health_test.go @@ -62,14 +62,14 @@ func TestColorizeTempThresholds(t *testing.T) { temp float64 expected string }{ - {temp: 30.0, expected: "30.0"}, // Normal - should use okStyle (green) - {temp: 55.9, expected: "55.9"}, // Just below warning threshold - {temp: 56.0, expected: "56.0"}, // Warning threshold - should use warnStyle (yellow) - {temp: 65.0, expected: "65.0"}, // Mid warning range - {temp: 75.9, expected: "75.9"}, // Just below danger threshold - {temp: 76.0, expected: "76.0"}, // Danger threshold - should use dangerStyle (red) - {temp: 90.0, expected: "90.0"}, // High temperature - {temp: 0.0, expected: "0.0"}, // Edge case: zero + {temp: 30.0, expected: "30.0"}, // Normal - should use okStyle (green) + {temp: 55.9, expected: "55.9"}, // Just below warning threshold + {temp: 56.0, expected: "56.0"}, // Warning threshold - should use warnStyle (yellow) + {temp: 65.0, expected: "65.0"}, // Mid warning range + {temp: 75.9, expected: "75.9"}, // Just below danger threshold + {temp: 76.0, expected: "76.0"}, // Danger threshold - should use dangerStyle (red) + {temp: 90.0, expected: "90.0"}, // High temperature + {temp: 0.0, expected: "0.0"}, // Edge case: zero } for _, tt := range tests { diff --git a/lib/core/app_protection.sh b/lib/core/app_protection.sh index 12d2ae8..70e0cb6 100755 --- a/lib/core/app_protection.sh +++ b/lib/core/app_protection.sh @@ -415,8 +415,8 @@ readonly DATA_PROTECTED_BUNDLES=( "com.netease.163music" # NetEase Music # Web Browsers (protect complex storage like IndexedDB, localStorage) - "Firefox" # Firefox Application Support - "org.mozilla.*" # Firefox bundle IDs + "Firefox" # Firefox Application Support + "org.mozilla.*" # Firefox bundle IDs # License Management & App Stores "com.paddle.Paddle*" # Paddle (license management) From 8a873c85a3c1a9def7751e840a0b343bcf65af08 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 14 Jan 2026 12:56:38 +0800 Subject: [PATCH 02/11] fix: remove unsafe file cleanup in Library root and fix tests - lib/clean: remove empty file cleanup in ~/Library to protect potential sentinel files - tests: fix unbound variable error in clean_user_core.bats by initializing WHITELIST_PATTERNS --- lib/clean/user.sh | 16 ---------------- tests/clean_user_core.bats | 1 + 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/clean/user.sh b/lib/clean/user.sh index ae44ef8..6e8f844 100644 --- a/lib/clean/user.sh +++ b/lib/clean/user.sh @@ -32,23 +32,7 @@ clean_empty_library_items() { safe_clean "${empty_dirs[@]}" "Empty Library folders" fi - # Clean empty files in Library root (skipping .localized and other sentinels) - local -a empty_files=() - while IFS= read -r -d '' file; do - [[ -f "$file" ]] || continue - # Protect .localized and potential system sentinels - if [[ "$(basename "$file")" == ".localized" ]]; then - continue - fi - if is_path_whitelisted "$file"; then - continue - fi - empty_files+=("$file") - done < <(find "$HOME/Library" -mindepth 1 -maxdepth 1 -type f -empty -print0 2> /dev/null) - if [[ ${#empty_files[@]} -gt 0 ]]; then - safe_clean "${empty_files[@]}" "Empty Library files" - fi # 2. Clean empty subdirectories in Application Support and other key locations # Iteratively remove empty directories until no more are found diff --git a/tests/clean_user_core.bats b/tests/clean_user_core.bats index 18e51a7..03be535 100644 --- a/tests/clean_user_core.bats +++ b/tests/clean_user_core.bats @@ -109,6 +109,7 @@ set -euo pipefail source "$PROJECT_ROOT/lib/core/common.sh" source "$PROJECT_ROOT/lib/clean/user.sh" safe_clean() { echo "$2"; } +WHITELIST_PATTERNS=() mkdir -p "$HOME/Library/EmptyDir" touch "$HOME/Library/empty.txt" clean_empty_library_items From 5708559d53c5765b82a230646ba548e923b873e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 04:57:34 +0000 Subject: [PATCH 03/11] chore: update contributors [skip ci] --- CONTRIBUTORS.svg | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.svg b/CONTRIBUTORS.svg index ed3ad88..231d88e 100644 --- a/CONTRIBUTORS.svg +++ b/CONTRIBUTORS.svg @@ -1,5 +1,5 @@ - - + + @@ -332,6 +332,17 @@ + + + + + + + + Copper-Eye + + + @@ -342,7 +353,7 @@ ClathW - + From 572c5c7b3b289add06a4e0a3e6ab42f48dbb4ff8 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 14 Jan 2026 05:01:12 +0000 Subject: [PATCH 04/11] chore: auto format code --- lib/clean/user.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/clean/user.sh b/lib/clean/user.sh index 6e8f844..014b1b6 100644 --- a/lib/clean/user.sh +++ b/lib/clean/user.sh @@ -32,8 +32,6 @@ clean_empty_library_items() { safe_clean "${empty_dirs[@]}" "Empty Library folders" fi - - # 2. Clean empty subdirectories in Application Support and other key locations # Iteratively remove empty directories until no more are found local -a key_locations=( From 62cfafd7da31cdc9c8c6c65ba1f81b9cedca075e Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 14 Jan 2026 14:09:08 +0800 Subject: [PATCH 05/11] Tighten dock removal and add brew uninstall fallback --- lib/core/common.sh | 19 +++++++------------ lib/uninstall/batch.sh | 10 ++++++++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/core/common.sh b/lib/core/common.sh index d34e415..0b4e6b8 100755 --- a/lib/core/common.sh +++ b/lib/core/common.sh @@ -177,9 +177,6 @@ remove_apps_from_dock() { local changed=false for target in "${targets[@]}"; do local app_path="$target" - local app_name - app_name=$(basename "$app_path" .app) - # Normalize path for comparison - realpath might fail if app is already deleted local full_path full_path=$(cd "$(dirname "$app_path")" 2> /dev/null && pwd || echo "") @@ -194,16 +191,14 @@ remove_apps_from_dock() { local url url=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps:$i:tile-data:file-data:_CFURLString" "$plist" 2> /dev/null || echo "") + [[ -z "$url" ]] && { ((i++)); continue; } - # Match by label or by path (parsing the CFURLString which is usually a file:// URL) - if [[ "$label" == "$app_name" ]] || [[ "$url" == *"$app_name.app"* ]]; then - # Double check path if possible to avoid false positives for similarly named apps - if [[ -n "$full_path" && "$url" == *"$full_path"* ]] || [[ "$label" == "$app_name" ]]; then - if /usr/libexec/PlistBuddy -c "Delete :persistent-apps:$i" "$plist" 2> /dev/null; then - changed=true - # After deletion, current index i now points to the next item - continue - fi + # Match by full path only to avoid removing apps with the same label. + if [[ -n "$full_path" && "$url" == *"$full_path"* ]]; then + if /usr/libexec/PlistBuddy -c "Delete :persistent-apps:$i" "$plist" 2> /dev/null; then + changed=true + # After deletion, current index i now points to the next item + continue fi fi ((i++)) diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh index 1d7e0ee..4b8c90b 100755 --- a/lib/uninstall/batch.sh +++ b/lib/uninstall/batch.sh @@ -373,15 +373,21 @@ batch_uninstall_applications() { if [[ "$is_brew_cask" == "true" && -n "$cask_name" ]]; then # Use brew uninstall --cask with progress indicator local brew_output_file=$(mktemp) + local brew_failed=false if ! run_with_timeout 120 brew uninstall --cask "$cask_name" > "$brew_output_file" 2>&1; then - # Fallback to manual removal if brew fails + brew_failed=true + log_warning "brew uninstall failed for $app_name, falling back to manual cleanup" + fi + rm -f "$brew_output_file" + if [[ "$brew_failed" == "true" ]]; then + [[ -z "$related_files" ]] && related_files=$(find_app_files "$bundle_id" "$app_name") + [[ -z "$system_files" ]] && system_files=$(find_app_system_files "$bundle_id" "$app_name") if [[ "$needs_sudo" == true ]]; then safe_sudo_remove "$app_path" || reason="remove failed" else safe_remove "$app_path" true || reason="remove failed" fi fi - rm -f "$brew_output_file" elif [[ "$needs_sudo" == true ]]; then if ! safe_sudo_remove "$app_path"; then local app_owner=$(get_file_owner "$app_path") From d0faaa33c60d59c5af45448de7876dab471e469e Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 14 Jan 2026 06:10:25 +0000 Subject: [PATCH 06/11] chore: auto format code --- lib/core/common.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/core/common.sh b/lib/core/common.sh index 0b4e6b8..cc74c63 100755 --- a/lib/core/common.sh +++ b/lib/core/common.sh @@ -191,7 +191,10 @@ remove_apps_from_dock() { local url url=$(/usr/libexec/PlistBuddy -c "Print :persistent-apps:$i:tile-data:file-data:_CFURLString" "$plist" 2> /dev/null || echo "") - [[ -z "$url" ]] && { ((i++)); continue; } + [[ -z "$url" ]] && { + ((i++)) + continue + } # Match by full path only to avoid removing apps with the same label. if [[ -n "$full_path" && "$url" == *"$full_path"* ]]; then From 5ec237b9dd1c9d554f8ba5dd1cd6977e133a7660 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 14 Jan 2026 15:26:42 +0800 Subject: [PATCH 07/11] publish 1.21 --- mole | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mole b/mole index b7e3bc6..353bce9 100755 --- a/mole +++ b/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.20.0" +VERSION="1.21.0" MOLE_TAGLINE="Deep clean and optimize your Mac." is_touchid_configured() { From cac29090930a85c51c661f2fad1266f4b7383c46 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Thu, 15 Jan 2026 09:51:56 +0800 Subject: [PATCH 08/11] fix: prevent Microsoft Teams from being misdetected as Edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #313 Change browser process detection from pgrep -f (full command line match) to pgrep -x (exact process name match) to prevent false positives. Microsoft Teams processes contain 'Microsoft' in their paths and may have Chromium-based components, which was causing them to be incorrectly identified as Microsoft Edge during clean operations. Changes: - Chrome detection: pgrep -f → pgrep -x - Edge detection: pgrep -f → pgrep -x - Edge updater detection: pgrep -f → pgrep -x This approach is consistent with Firefox detection and prevents Apps like Microsoft Teams, Microsoft Office, or other Microsoft products from triggering false Edge detection. All existing tests pass (9/9 in clean_browser_versions.bats). --- lib/clean/user.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/clean/user.sh b/lib/clean/user.sh index 014b1b6..82d2b6d 100644 --- a/lib/clean/user.sh +++ b/lib/clean/user.sh @@ -84,8 +84,8 @@ clean_chrome_old_versions() { "$HOME/Applications/Google Chrome.app" ) - # Use -f to match Chrome Helper processes as well - if pgrep -f "Google Chrome" > /dev/null 2>&1; then + # Match the exact Chrome process name to avoid false positives + if pgrep -x "Google Chrome" > /dev/null 2>&1; then echo -e " ${YELLOW}${ICON_WARNING}${NC} Google Chrome running · old versions cleanup skipped" return 0 fi @@ -164,8 +164,8 @@ clean_edge_old_versions() { "$HOME/Applications/Microsoft Edge.app" ) - # Use -f to match Edge Helper processes as well - if pgrep -f "Microsoft Edge" > /dev/null 2>&1; then + # Match the exact Edge process name to avoid false positives (e.g., Microsoft Teams) + if pgrep -x "Microsoft Edge" > /dev/null 2>&1; then echo -e " ${YELLOW}${ICON_WARNING}${NC} Microsoft Edge running · old versions cleanup skipped" return 0 fi @@ -242,7 +242,7 @@ clean_edge_updater_old_versions() { local updater_dir="$HOME/Library/Application Support/Microsoft/EdgeUpdater/apps/msedge-stable" [[ -d "$updater_dir" ]] || return 0 - if pgrep -f "Microsoft Edge" > /dev/null 2>&1; then + if pgrep -x "Microsoft Edge" > /dev/null 2>&1; then echo -e " ${YELLOW}${ICON_WARNING}${NC} Microsoft Edge running · updater cleanup skipped" return 0 fi From c34d91b36fbc73c699600ffc7b4d7da6f2551780 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Thu, 15 Jan 2026 11:39:33 +0800 Subject: [PATCH 09/11] feat: enhance uninstall with launch items and login items cleanup - Add automatic cleanup of LaunchAgents/Daemons (Issue #315) - Support both system and user-level launch paths - Add Login Items cleanup (fixing broken entries like CodexBar) - Improve Homebrew uninstall logging visibility - Update security audit and tests --- SECURITY_AUDIT.md | 24 +++++++++--------- lib/core/app_protection.sh | 18 ++++++++++---- lib/uninstall/batch.sh | 50 +++++++++++++++++++++++++++++++++++--- tests/uninstall.bats | 8 ++++++ 4 files changed, 79 insertions(+), 21 deletions(-) diff --git a/SECURITY_AUDIT.md b/SECURITY_AUDIT.md index e4b4013..4aa089c 100644 --- a/SECURITY_AUDIT.md +++ b/SECURITY_AUDIT.md @@ -2,7 +2,7 @@
-**Status:** PASSED | **Risk Level:** LOW | **Version:** 1.19.0 (2026-01-09) +**Status:** PASSED | **Risk Level:** LOW | **Version:** 1.21.0 (2026-01-15)
@@ -12,9 +12,9 @@ | Attribute | Details | |-----------|---------| -| Audit Date | January 9, 2026 | +| Audit Date | January 15, 2026 | | Audit Conclusion | **PASSED** | -| Mole Version | V1.19.0 | +| Mole Version | V1.21.0 | | Audited Branch | `main` (HEAD) | | Scope | Shell scripts, Go binaries, Configuration | | Methodology | Static analysis, Threat modeling, Code review | @@ -176,18 +176,18 @@ For user-selected app removal: | AI & LLM Tools | Cursor, Claude, ChatGPT, Ollama, LM Studio | Protects models, tokens, and sessions | | Startup Items | `com.apple.*` LaunchAgents/Daemons | System items unconditionally skipped | -**Orphaned Helper Cleanup (`opt_startup_items_cleanup`):** +**LaunchAgent/LaunchDaemon Cleanup During Uninstallation:** -Removes LaunchAgents/Daemons whose associated app has been uninstalled: +When users uninstall applications via `mo uninstall`, Mole automatically removes associated LaunchAgent and LaunchDaemon plists: -- Checks `AssociatedBundleIdentifiers` to detect orphans. -- Skips all `com.apple.*` system items. -- Skips paths under `/System/*`, `/usr/bin/*`, `/usr/lib/*`, `/usr/sbin/*`, `/Library/Apple/*`. -- Uses `safe_remove` / `safe_sudo_remove` with path validation. -- Unloads service via `launchctl` before deletion. -- **Timeout Protection:** 10-second limit on `mdfind` operations. +- Scans `~/Library/LaunchAgents`, `~/Library/LaunchDaemons`, `/Library/LaunchAgents`, `/Library/LaunchDaemons` +- Matches both exact bundle ID (`com.example.app.plist`) and app name patterns (`*AppName*.plist`) +- Skips all `com.apple.*` system items via `should_protect_path()` validation +- Unloads services via `launchctl` before deletion (via `stop_launch_services()`) +- **Safer than orphan detection:** Only removes plists when the associated app is explicitly being uninstalled +- Prevents accumulation of orphaned startup items that persist after app removal -**Code:** `lib/optimize/tasks.sh:opt_startup_items_cleanup()` +**Code:** `lib/core/app_protection.sh:find_app_files()`, `lib/uninstall/batch.sh:stop_launch_services()` ### Crash Safety & Atomic Operations diff --git a/lib/core/app_protection.sh b/lib/core/app_protection.sh index 70e0cb6..b908a06 100755 --- a/lib/core/app_protection.sh +++ b/lib/core/app_protection.sh @@ -660,6 +660,7 @@ find_app_files() { "$HOME/Library/HTTPStorages/$bundle_id" "$HOME/Library/Cookies/$bundle_id.binarycookies" "$HOME/Library/LaunchAgents/$bundle_id.plist" + "$HOME/Library/LaunchDaemons/$bundle_id.plist" "$HOME/Library/Application Scripts/$bundle_id" "$HOME/Library/Services/$app_name.workflow" "$HOME/Library/QuickLook/$app_name.qlgenerator" @@ -734,11 +735,18 @@ find_app_files() { fi fi - # Launch Agents by name (special handling) - if [[ ${#app_name} -gt 3 ]] && [[ -d ~/Library/LaunchAgents ]]; then - while IFS= read -r -d '' plist; do - files_to_clean+=("$plist") - done < <(command find ~/Library/LaunchAgents -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null) + # Launch Agents and Daemons by name (special handling) + if [[ ${#app_name} -gt 3 ]]; then + if [[ -d ~/Library/LaunchAgents ]]; then + while IFS= read -r -d '' plist; do + files_to_clean+=("$plist") + done < <(command find ~/Library/LaunchAgents -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null) + fi + if [[ -d ~/Library/LaunchDaemons ]]; then + while IFS= read -r -d '' plist; do + files_to_clean+=("$plist") + done < <(command find ~/Library/LaunchDaemons -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null) + fi fi # Handle specialized toolchains and development environments diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh index 4b8c90b..04dfa6a 100755 --- a/lib/uninstall/batch.sh +++ b/lib/uninstall/batch.sh @@ -88,6 +88,40 @@ stop_launch_services() { fi } +# Remove macOS Login Items for an app +remove_login_item() { + local app_name="$1" + local bundle_id="$2" + + # Skip if no identifiers provided + [[ -z "$app_name" && -z "$bundle_id" ]] && return 0 + + # Strip .app suffix if present (login items don't include it) + local clean_name="${app_name%.app}" + + # Remove from Login Items using index-based deletion (handles broken items) + if [[ -n "$clean_name" ]]; then + osascript <<-EOF 2>/dev/null || true + tell application "System Events" + try + set itemCount to count of login items + -- Delete in reverse order to avoid index shifting + repeat with i from itemCount to 1 by -1 + try + set itemName to name of login item i + if itemName is "$clean_name" then + delete login item i + end if + end try + end repeat + end try + end tell + EOF + fi +} + + + # Remove files (handles symlinks, optional sudo). remove_file_list() { local file_list="$1" @@ -364,6 +398,9 @@ batch_uninstall_applications() { [[ -n "$system_files" ]] && has_system_files="true" stop_launch_services "$bundle_id" "$has_system_files" + # Remove from Login Items + remove_login_item "$app_name" "$bundle_id" + if ! force_kill_app "$app_name" "$app_path"; then reason="still running" fi @@ -371,15 +408,20 @@ batch_uninstall_applications() { # Remove the application only if not running. if [[ -z "$reason" ]]; then if [[ "$is_brew_cask" == "true" && -n "$cask_name" ]]; then - # Use brew uninstall --cask with progress indicator - local brew_output_file=$(mktemp) + # Stop spinner before brew output + if [[ -t 1 ]]; then + stop_inline_spinner + fi + + # Use brew uninstall --cask - show output directly local brew_failed=false - if ! run_with_timeout 120 brew uninstall --cask "$cask_name" > "$brew_output_file" 2>&1; then + if ! run_with_timeout 120 brew uninstall --cask "$cask_name" 2>&1; then brew_failed=true log_warning "brew uninstall failed for $app_name, falling back to manual cleanup" fi - rm -f "$brew_output_file" + if [[ "$brew_failed" == "true" ]]; then + # Fallback to manual cleanup [[ -z "$related_files" ]] && related_files=$(find_app_files "$bundle_id" "$app_name") [[ -z "$system_files" ]] && system_files=$(find_app_system_files "$bundle_id" "$app_name") if [[ "$needs_sudo" == true ]]; then diff --git a/tests/uninstall.bats b/tests/uninstall.bats index 080e081..59652d3 100644 --- a/tests/uninstall.bats +++ b/tests/uninstall.bats @@ -37,6 +37,10 @@ create_app_artifacts() { mkdir -p "$HOME/Library/Preferences/ByHost" touch "$HOME/Library/Preferences/ByHost/com.example.TestApp.ABC123.plist" mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState" + mkdir -p "$HOME/Library/LaunchAgents" + touch "$HOME/Library/LaunchAgents/com.example.TestApp.plist" + mkdir -p "$HOME/Library/LaunchDaemons" + touch "$HOME/Library/LaunchDaemons/com.example.TestApp.plist" } @test "find_app_files discovers user-level leftovers" { @@ -55,6 +59,8 @@ EOF [[ "$result" == *"Preferences/com.example.TestApp.plist"* ]] [[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]] [[ "$result" == *"Containers/com.example.TestApp"* ]] + [[ "$result" == *"LaunchAgents/com.example.TestApp.plist"* ]] + [[ "$result" == *"LaunchDaemons/com.example.TestApp.plist"* ]] } @test "calculate_total_size returns aggregate kilobytes" { @@ -114,6 +120,8 @@ batch_uninstall_applications [[ ! -d "$HOME/Library/Application Support/TestApp" ]] || exit 1 [[ ! -d "$HOME/Library/Caches/TestApp" ]] || exit 1 [[ ! -f "$HOME/Library/Preferences/com.example.TestApp.plist" ]] || exit 1 +[[ ! -f "$HOME/Library/LaunchAgents/com.example.TestApp.plist" ]] || exit 1 +[[ ! -f "$HOME/Library/LaunchDaemons/com.example.TestApp.plist" ]] || exit 1 EOF [ "$status" -eq 0 ] From 9e1d09cb938c057a8590eeb47ea4452273972576 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Thu, 15 Jan 2026 11:40:49 +0800 Subject: [PATCH 10/11] fix: handle spaces in dock item removal - URL-encode paths when matching against Dock persistent-apps - Fixes issue where apps with spaces in names (e.g. 'Clash Party') were not removed from Dock --- lib/core/common.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/core/common.sh b/lib/core/common.sh index cc74c63..6e847ab 100755 --- a/lib/core/common.sh +++ b/lib/core/common.sh @@ -182,6 +182,9 @@ remove_apps_from_dock() { full_path=$(cd "$(dirname "$app_path")" 2> /dev/null && pwd || echo "") [[ -n "$full_path" ]] && full_path="$full_path/$(basename "$app_path")" + # URL-encode the path for matching against Dock URLs (spaces -> %20) + local encoded_path="${full_path// /%20}" + # Find the index of the app in persistent-apps local i=0 while true; do @@ -196,8 +199,8 @@ remove_apps_from_dock() { continue } - # Match by full path only to avoid removing apps with the same label. - if [[ -n "$full_path" && "$url" == *"$full_path"* ]]; then + # Match by URL-encoded path to handle spaces in app names + if [[ -n "$encoded_path" && "$url" == *"$encoded_path"* ]]; then if /usr/libexec/PlistBuddy -c "Delete :persistent-apps:$i" "$plist" 2> /dev/null; then changed=true # After deletion, current index i now points to the next item From 318c67ffbee929422b294351e5886282d5d4757a Mon Sep 17 00:00:00 2001 From: Tw93 Date: Thu, 15 Jan 2026 11:41:16 +0800 Subject: [PATCH 11/11] perf: optimize search filter rendering in paginated menu - Use partial redraw for search input updates instead of full screen refresh - Reduces flickering when typing in the filter box - Improve responsiveness of search interaction --- lib/ui/menu_paginated.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/ui/menu_paginated.sh b/lib/ui/menu_paginated.sh index cce6c96..2dc5cd0 100755 --- a/lib/ui/menu_paginated.sh +++ b/lib/ui/menu_paginated.sh @@ -864,7 +864,14 @@ paginated_multi_select() { # Backspace filter if [[ "$filter_mode" == "true" && -n "$filter_query" ]]; then filter_query="${filter_query%?}" - need_full_redraw=true + # Fast footer-only update in filter mode (avoid full redraw) + local filter_status="${filter_query:-_}" + local footer_row=$((items_per_page + 4)) + printf "\033[%d;1H\033[2K" "$footer_row" >&2 + local sep=" ${GRAY}|${NC} " + printf "%s" "${GRAY}Search: ${filter_status}${NC}${sep}${GRAY}Delete${NC}${sep}${GRAY}Enter Confirm${NC}${sep}${GRAY}ESC Cancel${NC}" >&2 + printf "\033[%d;1H\033[2K" "$((footer_row + 1))" >&2 + continue fi ;; CHAR:*) @@ -873,7 +880,14 @@ paginated_multi_select() { # avoid accidental leading spaces if [[ -n "$filter_query" || "$ch" != " " ]]; then filter_query+="$ch" - need_full_redraw=true + # Fast footer-only update in filter mode (avoid full redraw) + local filter_status="${filter_query:-_}" + local footer_row=$((items_per_page + 4)) + printf "\033[%d;1H\033[2K" "$footer_row" >&2 + local sep=" ${GRAY}|${NC} " + printf "%s" "${GRAY}Search: ${filter_status}${NC}${sep}${GRAY}Delete${NC}${sep}${GRAY}Enter Confirm${NC}${sep}${GRAY}ESC Cancel${NC}" >&2 + printf "\033[%d;1H\033[2K" "$((footer_row + 1))" >&2 + continue fi fi ;;