From a77638da8d122f45681d930f8fd34f760cd5d869 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Mon, 15 Dec 2025 15:32:49 +0800 Subject: [PATCH 01/18] feat: Add `com.apple.finder` cache to cleaning, protect specific apps from critical cleaning, implement login item whitelist checks --- bin/clean.sh | 2 +- lib/core/base.sh | 1 + lib/optimize/maintenance.sh | 3 +++ mole | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bin/clean.sh b/bin/clean.sh index 0cf8974..14c8d27 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -223,7 +223,7 @@ safe_clean() { # Hard-coded protection for critical apps (cannot be disabled by user) case "$path" in - *clash* | *Clash* | *surge* | *Surge* | *mihomo* | *openvpn* | *OpenVPN* | *verge* | *Verge* | *shadowsocks* | *Shadowsocks* | *v2ray* | *V2Ray* | *sing-box* | *tailscale* | *nordvpn* | *NordVPN* | *expressvpn* | *ExpressVPN* | *protonvpn* | *ProtonVPN* | *mullvad* | *Mullvad* | *hiddify* | *Hiddify* | *loon* | *Loon* | *Cursor* | *cursor* | *Claude* | *claude* | *ChatGPT* | *chatgpt* | *Ollama* | *ollama* | *lmstudio* | *Chatbox* | *Gemini* | *gemini* | *Perplexity* | *perplexity* | *Windsurf* | *windsurf* | *Poe* | *poe* | *DiffusionBee* | *diffusionbee* | *DrawThings* | *drawthings*) + *clash* | *Clash* | *surge* | *Surge* | *mihomo* | *openvpn* | *OpenVPN* | *verge* | *Verge* | *shadowsocks* | *Shadowsocks* | *v2ray* | *V2Ray* | *sing-box* | *tailscale* | *nordvpn* | *NordVPN* | *expressvpn* | *ExpressVPN* | *protonvpn* | *ProtonVPN* | *mullvad* | *Mullvad* | *hiddify* | *Hiddify* | *loon* | *Loon* | *Cursor* | *cursor* | *Claude* | *claude* | *ChatGPT* | *chatgpt* | *Ollama* | *ollama* | *lmstudio* | *Chatbox* | *Gemini* | *gemini* | *Perplexity* | *perplexity* | *Windsurf* | *windsurf* | *Poe* | *poe* | *DiffusionBee* | *diffusionbee* | *DrawThings* | *drawthings* | *Aerial* | *aerial* | *Fliqlo* | *fliqlo* | *com.apple.finder*) skip=true ((skipped_count++)) ;; diff --git a/lib/core/base.sh b/lib/core/base.sh index 6ec235d..eae2e80 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -82,6 +82,7 @@ declare -a DEFAULT_WHITELIST_PATTERNS=( "$HOME/Library/Caches/org.R-project.R/R/renv/*" "$HOME/Library/Caches/JetBrains*" "$HOME/Library/Caches/com.jetbrains.toolbox*" + "$HOME/Library/Caches/com.apple.finder" "$FINDER_METADATA_SENTINEL" ) diff --git a/lib/optimize/maintenance.sh b/lib/optimize/maintenance.sh index d21e5a7..0e8a685 100644 --- a/lib/optimize/maintenance.sh +++ b/lib/optimize/maintenance.sh @@ -74,6 +74,9 @@ fix_broken_login_items() { local launch_agents_dir="$HOME/Library/LaunchAgents" [[ -d "$launch_agents_dir" ]] || return 0 + # Check whitelist + if command -v is_whitelisted > /dev/null && is_whitelisted "check_login_items"; then return 0; fi + local broken_count=0 while IFS= read -r plist_file; do diff --git a/mole b/mole index 5225ad2..f7a687f 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" # Version info -VERSION="1.13.0" +VERSION="1.13.1" MOLE_TAGLINE="can dig deep to clean your Mac." # Check if Touch ID is already configured From abaf47edd6637dc23aa51570bcd35e647df04bf1 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Mon, 15 Dec 2025 07:34:06 +0000 Subject: [PATCH 02/18] chore: update binaries for V1.13.1 --- bin/analyze-go | Bin 7298178 -> 7298178 bytes bin/status-go | Bin 7422050 -> 7422050 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/analyze-go b/bin/analyze-go index 1d1ab14b694271d92df4573a18194c824bb2f0c0..06ea96bd57aaefec5ee01575bd77ed4676607f0d 100755 GIT binary patch delta 1243 zcmcK2NlX&~00q#vf#6nLC>6yDqNtSVOsAb0+^8Uw7CJ49jCHAPr_GRGLJpJhaNz$eEfnPC_(Q)CIjH&9?@c zBh7S$)5``3UGZi?Fz}_NUcv1S8f~;h1qhmKaYQUFkub}+iusmGliyyOovQxTxl^s$ z6;;Qg>Jcy!M!{$p14)n!V<81nVH}Ky2`~{RfdbNCGE9M~Fb&c{2^pY*>2g$^c|2*V zUd!kxn$x%##-L+G7b}vKOwP+(mz?)!L`k&Xz>*@T(@Lx)>S&2EXg#7{WDQ(At6fs1 z>T9%2p^hq~xwx@inid->$b=a%6S80ys9`o_Lk`S=T$l^=-=6&b}B8qHng-M9V+mA*%Fj>+9+tbc5E zd6=sTNBAbY&0Sp;6s(4lW|1f*Ifu<_Zf+7xhNQ%H`QqW93&kj3sux zIcOvac8lBR;H_rXEQEzdmsRWZY>0VmEO1Z=i(oMv!!!Esq~!G9Jo5V4C#!E;?}?Ka2bjmoj@;qn9kP38^Oa>iiSO&> Jge7!K@f$as#rpsN delta 1243 zcmcK2S!@ge90qV_ODU_au2Q2#OKs=ab5={Ydv;rU^_s4-Iy*b6d+U}u!XoS@JP|gU zN<2A&L<=feRW%N!stIL;->e=7) zaqlwA@YdHokEJiS`{+yA`w3ZdAQWNgj+lvc28||f)Z-K!?PSDO@2Ivb5>|6!^M*L% zGlWiznUD{1m?b5Nf+JbeR5ZiT z6s2G(K@(tkTE%OX5?xZTI=l4GlTy^Ed7Vn>SF?PZO2epGzlg_xK=FZ0VOo^us2XTS z&(L~`${5n(^weN^0nCOuPzZB@fO${^#V{Whz(Od2Qdk6wp$wLQ0?MVqa`JHBe~X(z zTzMvz7SZJ!5-6a70VSwF4H_x=Mw{=G?WlZM8ELHOzIWx8+eUxdCY`YA+Q-#961BcC zWvz?ajV^_m3U*Ra59#w5ySwa;P0hM+SCi9hs*Q3kG8_moZT|HQ4yV~`FuB$Cpn)bg z)R=;up_X`zb-QAXigj)7L@4EV>3{`2EQJcFgk`WCs=xr%U<4DG!2(vWK@HfU7V2OH zIKT-m$?tN1fByG&t?$Y> zvaWqd^X%t4@z_l1`7YPfzM2vGS4UQS7}5H5NmuJWZ*E=#l-M)rhj91K;fv`Pxn-yNow0A6W!1ohEjQo#9;_u_#1aox zBQa>=a`9jc;UG~@BgCkD(0FXTOY)nUfUM{B-7#lKR_t|@Llzq8W{1U; zh?^6Et}yRqc2Xu!T^G5H?PHtV`@%hnS?NneD3hO!$M^OG$%aHAtXLv;uic@|BYvLh zCrS<{iByuvfEh3oW1g|G;Up#&DINuu;% zMiEPMjNTxSK8`aoyyW90onF;wOYdfv{e9904XlyZNdiL$`JluYf}D{KNUX#gg)=+i zVo4s`LUUD&zRDn+>5q$4%3fXyOJFH1gXKWL3edtzSOu$L4XlMSSO?`$0qcQ;N}yDG zIW6p;zR2leI+Y&QfgTtz01HOo)buc4-Kj~BJ%2IvZsUf>9nZ$JRQn^fELWJy5F4zd z%@>I=H7sd$1iPjEN=uybxI=ZJa4ktTMp|65t-e)J+>~2#hGK~>n)Y(_7RKZa+iO~> zX0EnZ_6rWu7xsH>qP;%YYf1(31*n1zun{)FW~hcO@cYLMTcHMOp$;ryg?eZJ8#F=_ z*ueo#a6z*g$QQpz|L#?m#ryw1g%qX$E#QW2;DJ`~LK|#{cIbc|uoF7L2Yv{E1iK&z zU21=PchriKodi#ld%Rm%Qy%!4;LZy=!y%9ZHLc$%Gp9^VYH}oU-Y0wHR}|j;1?_Voge@J delta 1177 zcmcK2OGp#}90qXAd{=I%saZN%nQ7b1?9Qx?Wxm$k@wKkI?n}$Z%sw>NeUf(-V<0Z4;%$bf^830aU0homr(b1f#F zp%n_6qU0Q9S2;L=W`q)@B+Jj4PbmERq;#rSp_EaIEGKj`Dh11lPCPp7l;D`?2pO}J znO0g+rl89xYGOEKBqO?_95@U|AQ$q0fTNHP$KW`efC4xPg>VXrpcqbr9L@kK>53?B zaOWZ~Pg0TPNg5QO1O`}8L5Z|HSz6I0dq&7|XVyx$))U8beg^uoq{1X_BgQzSw|naC zq7d+Myg4{nKPV5FN!HL@B5(EA+N=)2>ty|Uw}vK7c71Pf$VE}Mk~*!j%I4Oot)!Rt z)ztW?{;Ldc?+;!cY;5yT5ix~>GB^w8pd8La1yn*6R6`A@p%yft1s>|49vYw#nm`B5 zpobPPNMed{%l_|PeLmx>|DQqxQ-D@5ff+1d1sk+MJ6wQ^&;gy$1>GQk2zGFQ6I{}8 zs3(4LC(`~I?h4-Z21AGo0(Jm8g9c;7;VC{V6?^MvS_lo6d6E$?586~7MLemLs; zxY;Qz-Ay-0vu}Je4Dsp1jr((x{M%e^=~w&s!s-LVkCFGnw-ItPui~Bhvs}6t=MNVX p@~KA+!56O+#)h}2HTh~5oy7nE From 50806efc3799cf747bf9d8afb3bcb9dad676e541 Mon Sep 17 00:00:00 2001 From: purofle Date: Tue, 16 Dec 2025 17:43:44 +0800 Subject: [PATCH 03/18] fix: correct boot_time parsing error (#130) --- lib/check/health_json.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/check/health_json.sh b/lib/check/health_json.sh index e8e58fe..dec4731 100644 --- a/lib/check/health_json.sh +++ b/lib/check/health_json.sh @@ -70,7 +70,7 @@ get_uptime_days() { local boot_output boot_time uptime_days boot_output=$(sysctl -n kern.boottime 2> /dev/null || echo "") - boot_time=$(echo "$boot_output" | sed -n 's/.*sec = \([0-9]*\).*/\1/p' 2> /dev/null || echo "") + boot_time=$(echo "$boot_output" | awk -F 'sec = |, usec' '{print $2}' 2> /dev/null || echo "") if [[ -n "$boot_time" && "$boot_time" =~ ^[0-9]+$ ]]; then local now=$(date +%s 2> /dev/null || echo "0") From 019a0c63ff7799bf5b2839eb58c9896c6277b454 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Tue, 16 Dec 2025 17:54:37 +0800 Subject: [PATCH 04/18] fix: Enhance sudo password input handling and expand tilde paths in maintenance. --- lib/core/sudo.sh | 13 ++++++++++++- lib/optimize/maintenance.sh | 3 +++ mole | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/core/sudo.sh b/lib/core/sudo.sh index da443db..617dcaf 100644 --- a/lib/core/sudo.sh +++ b/lib/core/sudo.sh @@ -42,6 +42,11 @@ _request_password() { # Extra safety: ensure sudo cache is cleared before password input sudo -k 2> /dev/null + # Save original terminal settings and ensure they're restored on exit + local stty_orig + stty_orig=$(stty -g < "$tty_path" 2> /dev/null || echo "") + trap '[[ -n "$stty_orig" ]] && stty "$stty_orig" < "$tty_path" 2> /dev/null || true' RETURN + while ((attempts < 3)); do local password="" @@ -52,7 +57,13 @@ _request_password() { fi printf "${PURPLE}${ICON_ARROW}${NC} Password: " > "$tty_path" - IFS= read -r -s password < "$tty_path" || password="" + + # Disable terminal echo to hide password input + stty -echo -icanon min 1 time 0 < "$tty_path" 2> /dev/null || true + IFS= read -r password < "$tty_path" || password="" + # Restore terminal echo immediately + stty echo icanon < "$tty_path" 2> /dev/null || true + printf "\n" > "$tty_path" if [[ -z "$password" ]]; then diff --git a/lib/optimize/maintenance.sh b/lib/optimize/maintenance.sh index 0e8a685..2baaee6 100644 --- a/lib/optimize/maintenance.sh +++ b/lib/optimize/maintenance.sh @@ -100,6 +100,9 @@ fix_broken_login_items() { program=$(plutil -extract ProgramArguments.0 raw "$plist_file" 2> /dev/null || echo "") fi + # Expand tilde in path if present + program="${program/#\~/$HOME}" + # Skip if no program found or program exists [[ -z "$program" ]] && continue [[ -e "$program" ]] && continue diff --git a/mole b/mole index f7a687f..90e5f48 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" # Version info -VERSION="1.13.1" +VERSION="1.13.2" MOLE_TAGLINE="can dig deep to clean your Mac." # Check if Touch ID is already configured From 470d376f1c168eb77087f34902521b7f0f1c6411 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Tue, 16 Dec 2025 09:56:00 +0000 Subject: [PATCH 05/18] chore: update binaries for V1.13.2 --- bin/analyze-go | Bin 7298178 -> 7298178 bytes bin/status-go | Bin 7422050 -> 7422050 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/analyze-go b/bin/analyze-go index 06ea96bd57aaefec5ee01575bd77ed4676607f0d..a5d7c0a89a0391b478d1cee67a2aca00f2cc57fa 100755 GIT binary patch delta 1243 zcmcK2TWm`K00!VNyO>+Cu^QXa8FT5*X;05>Zadq$)wZtPwAziCp3@%H+q6b!$)X$5 z5*}DsNQei)12G9XB!sZcCBeiC9wa1UZdrJ6OI-d(58lMX_wqme`SbT)xt7)o1ALz5 zF{Rso)^6y1+#3HjdNW#snT;^K2#qO#U&?Iy(-AYIO8uPeMo=;S-v;@LLJnD z6&k<>c2%}Fet-V=b)#fc%fFq$n6z+Buo@iTgf-v-H#9>Ftc7*Z3hSW_Hh>2tkiiQ+ z)xs&|tNusbB)$t9!4I1t06_>rSQS%|7fSizM^lH`qDbs^_sRD6Z(dr`+ZRWPjAdhn zw1?sEuy>IS2NH2eV=S)X4yPDo{kKw(r6?>mzOxlzB?W3 z8UFF>75$}f@7IpH^p_38=Mw{a=l1P><9~SKc)|9&bBd2nNVNtoI`7emkJ$(IzsRT= zcyi7+)O7Wesw-+wln^;-AG&*v_3rDw|7!5;qC4DY@AO+sg72bS>~-;}tV@^Fn|19s GG`|6kRl%YF delta 1243 zcmcK2NlX&~00q#vf#6nLC>6yDqNtSVOsAb0+^8Uw7CJ49jCHAPr_GRGLJpJhaNz$eEfnPC_(Q)CIjH&9?@c zBh7S$)5``3UGZi?Fz}_NUcv1S8f~;h1qhmKaYQUFkub}+iusmGliyyOovQxTxl^s$ z6;;Qg>Jcy!M!{$p14)n!V<81nVH}Ky2`~{RfdbNCGE9M~Fb&c{2^pY*>2g$^c|2*V zUd!kxn$x%##-L+G7b}vKOwP+(mz?)!L`k&Xz>*@T(@Lx)>S&2EXg#7{WDQ(At6fs1 z>T9%2p^hq~xwx@inid->$b=a%6S80ys9`o_Lk`S=T$l^=-=6&b}B8qHng-M9V+mA*%Fj>+9+tbc5E zd6=sTNBAbY&0Sp;6s(4lW|1f*Ifu<_Zf+7xhNQ%H`QqW93&kj3sux zIcOvac8lBR;H_rXEQEzdmsRWZY>0VmEO1Z=i(oMv!!!Esq~!G9Jo5V4C#!E;?}?Ka2bjmoj@;qn9kP38^Oa>iiSO&> Jge7!K@f$as#rpsN diff --git a/bin/status-go b/bin/status-go index 0aedee7af63956667466a66a8ff943a53ba215a7..ee8c0c0e7adc7cdb0b873f28b969bd977a319959 100755 GIT binary patch delta 1177 zcmcK2X>5xD9LI4pHa17u95u(YVWv~Po^u%Mu^zNmUC)i`^K_kRw_bV}kFxQ)9Z)D~OiG+kiVv&dhzr-6aypnkFdGmku%Rk?@`3I$M;X1oi zvi*eow}jAT|P)i+_cf2FnAAI0yOXRXW6WV<>HDSwIxRyW4VZ` zH(>YJ{ZY5vrD$=tcFSEfYg(f-$CzX`E!0&IKTr1) z(t$it$P*!|(v_PvFh9@a;BU@g?cI%t6PAcIEO0CLy}3ehH0 za+&`(d7`K+OcYh11`Uuv0S$~eQDnF7mB$W%908}XGqy7tA~uIGOEAJ6&dZd)8ObU_HZ z#s1vBvPb`8?eB0WP2s|%X+K0D3O&#ZF>%rqe^?;)+0Ku2@<#eYq{{l{_}HmohC8%| z`I@;Yt-4~qHNc4X#^QHL;@o&!=Bgn*EIE4ea8<3r8h*X5EajW_(e<)1`TW@Bka%fk tBHu_ndog(=+5avzI`~81`*i%%&$Y?jY{%%=+EAeW8vAKV)T$E1{9iZUoj?Ep delta 1177 zcmcK2NlX&~7zN;#1aox zBQa>=a`9jc;UG~@BgCkD(0FXTOY)nUfUM{B-7#lKR_t|@Llzq8W{1U; zh?^6Et}yRqc2Xu!T^G5H?PHtV`@%hnS?NneD3hO!$M^OG$%aHAtXLv;uic@|BYvLh zCrS<{iByuvfEh3oW1g|G;Up#&DINuu;% zMiEPMjNTxSK8`aoyyW90onF;wOYdfv{e9904XlyZNdiL$`JluYf}D{KNUX#gg)=+i zVo4s`LUUD&zRDn+>5q$4%3fXyOJFH1gXKWL3edtzSOu$L4XlMSSO?`$0qcQ;N}yDG zIW6p;zR2leI+Y&QfgTtz01HOo)buc4-Kj~BJ%2IvZsUf>9nZ$JRQn^fELWJy5F4zd z%@>I=H7sd$1iPjEN=uybxI=ZJa4ktTMp|65t-e)J+>~2#hGK~>n)Y(_7RKZa+iO~> zX0EnZ_6rWu7xsH>qP;%YYf1(31*n1zun{)FW~hcO@cYLMTcHMOp$;ryg?eZJ8#F=_ z*ueo#a6z*g$QQpz|L#?m#ryw1g%qX$E#QW2;DJ`~LK|#{cIbc|uoF7L2Yv{E1iK&z zU21=PchriKodi#ld%Rm%Qy%!4;LZy=!y%9ZHLc$%Gp9^VYH}oU-Y0wHR}|j;1?_Voge@J From bde4e16a43c1981b6e33e1733857cf3f283d8750 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Tue, 16 Dec 2025 18:03:14 +0800 Subject: [PATCH 06/18] update --- lib/manage/update.sh | 12 ++++++++++++ mole | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/manage/update.sh b/lib/manage/update.sh index 8f6d8fa..b61fa0f 100644 --- a/lib/manage/update.sh +++ b/lib/manage/update.sh @@ -289,6 +289,9 @@ _perform_appstore_update() { appstore_log=$(mktemp "${TMPDIR:-/tmp}/mole-appstore.XXXXXX" 2> /dev/null || echo "/tmp/mole-appstore.log") if [[ "$appstore_needs_fallback" == "true" ]]; then + # Ensure sudo session is active and valid before starting long-running operation + ensure_sudo_session "App Store updates require admin access" || return 1 + echo -e " ${GRAY}Installing all available updates${NC}" echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}" if sudo softwareupdate -i -a 2>&1 | tee "$appstore_log" | grep -v "^$"; then @@ -302,6 +305,9 @@ _perform_appstore_update() { echo -e "${RED}✗${NC} Software update failed" fi else + # Ensure sudo session is active and valid before starting long-running operation + ensure_sudo_session "App Store updates require admin access" || return 1 + echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}" if sudo softwareupdate -i "${appstore_labels[@]}" 2>&1 | tee "$appstore_log" | grep -v "^$"; then echo -e "${GREEN}✓${NC} App Store apps updated" @@ -324,6 +330,9 @@ _perform_macos_update() { macos_log=$(mktemp "${TMPDIR:-/tmp}/mole-macos.XXXXXX" 2> /dev/null || echo "/tmp/mole-macos.log") if [[ "$macos_needs_fallback" == "true" ]]; then + # Ensure sudo session is active and valid before starting long-running operation + ensure_sudo_session "macOS update requires admin access" || return 1 + echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}" if sudo softwareupdate -i -r 2>&1 | tee "$macos_log" | grep -v "^$"; then echo -e "${GREEN}✓${NC} macOS updated" @@ -332,6 +341,9 @@ _perform_macos_update() { echo -e "${RED}✗${NC} macOS update failed" fi else + # Ensure sudo session is active and valid before starting long-running operation + ensure_sudo_session "macOS update requires admin access" || return 1 + echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}" if sudo softwareupdate -i "${macos_labels[@]}" 2>&1 | tee "$macos_log" | grep -v "^$"; then echo -e "${GREEN}✓${NC} macOS updated" diff --git a/mole b/mole index 90e5f48..da659e8 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" # Version info -VERSION="1.13.2" +VERSION="1.13.3" MOLE_TAGLINE="can dig deep to clean your Mac." # Check if Touch ID is already configured From fa6d248578029530c7e5ef20c13aeeef90850979 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Tue, 16 Dec 2025 10:04:20 +0000 Subject: [PATCH 07/18] chore: update binaries for V1.13.3 --- bin/analyze-go | Bin 7298178 -> 7298178 bytes bin/status-go | Bin 7422050 -> 7422050 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/analyze-go b/bin/analyze-go index a5d7c0a89a0391b478d1cee67a2aca00f2cc57fa..2c5f688768499c0893a7f20da568895ae09a8341 100755 GIT binary patch delta 1235 zcmcK2TTD|290u@+4iGn}B8;{hoPkah;q>&Jwx_T`kA(ugo!+peDwnqB&Wnn;YBiP0F18jTMc5;2V$lVv2^zP;^XZ`&Sz`6vIU zFJHci3v)!-i7y|6v{vbt6>e4 z!&<0-N_ZctKn@BiO(>5SmQyMfO)-sukiZC(c95ZiltgV%_7&Ct{iSEAfL5Ssfzr`5 ztI-8TT2!-ih}CISx$3N_ET&Y=D!Q3sa$Hud$mE-p@Bw@X)ldTjd<3;n2kT%xd<+|) z9yY=zXn@Vo2u)JHiJU(7|76V}t33BvRz!;nB&dJ_HPFC-2DDOoL08o;8#wmm{I$i_ zqrd;!@@OCtPe|W)vf*O4-jeXx%`s=cwlfkl?@G904matK&^>ya-xFd?Je5>iy|%bW zjtIl*!Qkf}JKy2w+MOD6jHAd#lb`qc!}dmNnD;uZYTE74XMzqE{<_W30-wOA&(KltZ7i(F6Xas?&v|~w=EAIkg6A_ z^Rv(Iu9O1#c$zjW+bhk=?l&y%>{0HXD%`_3pC5MjuD`QNza;+HJYQ$6REPzkGwhq_ z!rA>lpSREPj@@66&0L?pbg^yzSxma#H}`#0Q#%meCa+*hXYbEMh#y{^ VE_j^}G{r|>-FPTnwInXf{sa>@zNr8J delta 1231 zcmcK2TWm`K00!VNyO>*Zadq$)wZtPwAziCp3@%H+q6b!$)X$5 z5*}DsNQei)12G9XB!sZcCBeiC9wa1UZdrJ6jm!V&!5bdFm;dR{pTFnQm6RUn=W{d< zDed0VHhs^-me`k(>#5>7{l)bCgvJ>N23-wGrH=FP!Md;*6FmNMveU~n)d_;buXtmW z=<0}CcoRuiw7XoawW`YNlB9rWGb#o@>2x%CoS{uZ2jAWrvvx);8|9ETlla-&P2_Hm z6Nxx63PwW;jDb{0gLKG%v5*PlU_4BKi7*K?Fd3#m7EFa~m_s;iA>Zxkpm$A}gr+m>3 zEX7tjoSo4Q3ttxy$t{*psDhO?3pHD8x=?x8!JF9ax|q ztYA}RTf?`ff1EZ#Ha7pw4Gal0*9a@Y4h~oaPH;gJG{b6G11+!?T45cyK>`^(;8o3> zBCh!7T@$!0tOp-#06zpE2q9HWhMy^A2OmrxWDCR5n_b7--oAQnPHkHlA<~wN8q7U# zXnw=+Rr|oM0-JW|RQ8U=udQM2?cVA+rmuTs3p2y&-u8HSNR>vyak{M7G5XEPX!p?f zpD*Z71-n1D*QP$NA37WF-#xo`=PTd+V@LD1-kw!-cwDl@f8KG2j=#^`xA$3Eb^oI? z-oeJpA5?8&TfCT~5FSvNCh+9RItVS;^r=GE8T2RZba>& z%Yy}ik3}?kG6PT0ON3=XkWvs!sHmV`f(VKbqrTBg4?RUad|rMJKfZkVzRuh$dJSXj zT4B^I9)DDW+gp#4!FV(4Y|&WK>X6YCN=dbnv|enn_lH}P z0WITksE@gHa;rq$-V_i!Wigr2F%StSG>KUHs3d$WO*=GoiIo0`n|0cWMISSWN~!M` zv(&m%87h~dc(4$PU=b{aV&FpwEP+xegQc(xmV*FO+9WwsO(krAjj#ze0|i?^1Y4mBw!wDT0oAY*YG4=Cf*5v#gfrF9 z^~wK~w~(iEg**dwKtKvIkb{CNX%bJ$>`rScGGJ)ylQ?{qzEnCu(=J7`j;LJ`Q)7pO zNE01FhdCJ3`V|3B#H$OrEje$Q66#?O>;)CCK@KATJ2=1z?a%=(=!AoC2)dvf+|UDu!2@3KfggGy zz-0%6{0IL$>aV@d>icrD`VfR60#P^uF>Y2LpUP1$C#O#?5V=3TSCWZ|$0PjCIq&J` z=dVq@-TmoB?VRZvcXv8|M@n72^f_w%_@sZ7m*ovD%Rjl@n6FB_OFtVb7U+k1Ealwg u#fi)=YWV57uYzxauD}tgONUNThtK7I`tj%cHhq8ITe!v+ zid!DleYsUrfBbRw!Yg{rIdffcQ#3+|?X=Dk)jIbZJ+!yoon(_LmW#;xl*SaP3#WpL zevidrafj?uo2=2^+#|J7Van7#5DG@s(ePkQrt}V~U3@5%XdX~$3}Ggg9N*d{_XvAc8zt2#X*e7Q+%)3I!mBWw0D1umV=XDp(DLPy}m)G*Nsg zM`1#qu1>(LAkW7%xeYqq|eq z;ygK5OOdrIW^^cJm1WFT#jqCE!Ft#L1e8E2Y=kl>hYHvPm9QDApc-mG3R^%Xn5*Qq ziT|{BDl5-SWfhNae%gP&NvK@Nz0a}6`Y3PQHg2X&4l)u?C_MXg_M}5CYxlJUl7l^Rxsx&IRE^G%xyd2( zg<6AlvppWzVKYc~C3QL1iqd}<_Oo%q`bGKc_nK|9#M4i0dF3p$_^x?nfBVGnq~3q1Is8~o5C z45fNS5C8epU%gK21DR?4UI;=6`k)`e!n8i}AVc(;&)wgW$aiRsO;rzgi5?m!vy zHF2Xza@jC8%m{a%MsAbD*~zxV745*d_{fQak`k>c@Oo2j+%@N;?PYcB`O!;$;o^d5 sx|(?QV)}4w=v{nb Date: Wed, 17 Dec 2025 07:09:05 +0800 Subject: [PATCH 08/18] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2adb3f..ed59746 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Mole

-

Dig deep like a mole to optimize your Mac.

+

Deep clean and optimize your Mac.

From b843cde0fdb0a2f93e4383d167d12fe9dd542104 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 10:36:33 +0800 Subject: [PATCH 09/18] feat: localize app names based on system language and improve UI display width calculation for CJK characters with loading indicator --- bin/uninstall.sh | 164 ++++++++++++++++++++--------------------- bin/uninstall_lib.sh | 137 +++++++++++++++------------------- lib/ui/app_selector.sh | 41 +++++++++-- 3 files changed, 174 insertions(+), 168 deletions(-) diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 3343966..4794a49 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -88,7 +88,13 @@ scan_applications() { fi fi - # Cache miss - show scanning feedback below + # Cache miss - prepare for scanning + local inline_loading=false + if [[ -t 1 && -t 2 ]]; then + inline_loading=true + # Clear screen for inline loading + printf "\033[2J\033[H" >&2 + fi local temp_file temp_file=$(create_temp_file) @@ -97,11 +103,7 @@ scan_applications() { local current_epoch current_epoch=$(date "+%s") - # Spinner for scanning feedback (simple ASCII for compatibility) - local spinner_chars="|/-\\" - local spinner_idx=0 - - # First pass: quickly collect all valid app paths and bundle IDs + # First pass: quickly collect all valid app paths and bundle IDs (NO mdls calls) local -a app_data_tuples=() while IFS= read -r -d '' app_path; do if [[ ! -e "$app_path" ]]; then continue; fi @@ -118,78 +120,19 @@ scan_applications() { continue fi - # Try to get English name from bundle info, fallback to folder name + # Get bundle ID only (fast, no mdls calls in first pass) local bundle_id="unknown" - local display_name="$app_name" if [[ -f "$app_path/Contents/Info.plist" ]]; then bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown") - - # Try to get English name from bundle info - local bundle_executable - bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null) - - # Smart display name selection - prefer descriptive names over generic ones - local candidates=() - - # Get all potential names - local bundle_display_name - bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null) - local bundle_name - bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null) - - # Check if executable name is generic/technical (should be avoided) - local is_generic_executable=false - if [[ -n "$bundle_executable" ]]; then - case "$bundle_executable" in - "pake" | "Electron" | "electron" | "nwjs" | "node" | "helper" | "main" | "app" | "binary") - is_generic_executable=true - ;; - esac - fi - - # Priority order for name selection: - # 1. App folder name (if ASCII and descriptive) - often the most complete name - if [[ "$app_name" =~ ^[A-Za-z0-9\ ._-]+$ && ${#app_name} -gt 3 ]]; then - candidates+=("$app_name") - fi - - # 2. CFBundleDisplayName (if meaningful and ASCII) - if [[ -n "$bundle_display_name" && "$bundle_display_name" =~ ^[A-Za-z0-9\ ._-]+$ ]]; then - candidates+=("$bundle_display_name") - fi - - # 3. CFBundleName (if meaningful and ASCII) - if [[ -n "$bundle_name" && "$bundle_name" =~ ^[A-Za-z0-9\ ._-]+$ && "$bundle_name" != "$bundle_display_name" ]]; then - candidates+=("$bundle_name") - fi - - # 4. CFBundleExecutable (only if not generic and ASCII) - if [[ -n "$bundle_executable" && "$bundle_executable" =~ ^[A-Za-z0-9._-]+$ && "$is_generic_executable" == false ]]; then - candidates+=("$bundle_executable") - fi - - # 5. Fallback to non-ASCII names if no ASCII found - if [[ ${#candidates[@]} -eq 0 ]]; then - [[ -n "$bundle_display_name" ]] && candidates+=("$bundle_display_name") - [[ -n "$bundle_name" && "$bundle_name" != "$bundle_display_name" ]] && candidates+=("$bundle_name") - candidates+=("$app_name") - fi - - # Select the first (best) candidate - display_name="${candidates[0]:-$app_name}" - - # Apply brand name mapping from common.sh - display_name="$(get_brand_name "$display_name")" fi # Skip system critical apps (input methods, system components) - # Note: Paid apps like CleanMyMac, 1Password are NOT protected here - users can uninstall them if should_protect_from_uninstall "$bundle_id"; then continue fi - # Store tuple: app_path|app_name|bundle_id|display_name - app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}") + # Store tuple: app_path|app_name|bundle_id (display_name will be resolved in parallel later) + app_data_tuples+=("${app_path}|${app_name}|${bundle_id}") done < <( # Scan both system and user application directories # Using maxdepth 3 to find apps in subdirectories (e.g., Adobe apps in /Applications/Adobe X/) @@ -209,11 +152,7 @@ scan_applications() { max_parallel=32 fi local pids=() - local inline_loading=false - if [[ "${MOLE_INLINE_LOADING:-}" == "1" || "${MOLE_INLINE_LOADING:-}" == "true" ]]; then - inline_loading=true - printf "\033[H" >&2 # Position cursor at top of screen - fi + # inline_loading variable already set above (line ~92) # Process app metadata extraction function process_app_metadata() { @@ -221,7 +160,35 @@ scan_applications() { local output_file="$2" local current_epoch="$3" - IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple" + IFS='|' read -r app_path app_name bundle_id <<< "$app_data_tuple" + + # Get localized display name (moved from first pass for better performance) + local display_name="$app_name" + if [[ -f "$app_path/Contents/Info.plist" ]]; then + # Try to get localized name from system metadata (best for i18n) + local md_display_name + md_display_name=$(run_with_timeout 0.05 mdls -name kMDItemDisplayName -raw "$app_path" 2> /dev/null || echo "") + + # Get bundle names + local bundle_display_name + bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null) + local bundle_name + bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null) + + # Priority order for name selection (prefer localized names): + # 1. System metadata display name (kMDItemDisplayName) - respects system language + # 2. CFBundleDisplayName - usually localized + # 3. CFBundleName - fallback + # 4. App folder name - last resort + + if [[ -n "$md_display_name" && "$md_display_name" != "(null)" && "$md_display_name" != "$app_name" ]]; then + display_name="$md_display_name" + elif [[ -n "$bundle_display_name" && "$bundle_display_name" != "(null)" ]]; then + display_name="$bundle_display_name" + elif [[ -n "$bundle_name" && "$bundle_name" != "(null)" ]]; then + display_name="$bundle_name" + fi + fi # Parallel size calculation local app_size="N/A" @@ -293,9 +260,9 @@ scan_applications() { local completed=$(cat "$progress_file" 2> /dev/null || echo 0) local c="${spinner_chars:$((i % 4)):1}" if [[ $inline_loading == true ]]; then - printf "\033[H\033[2K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2 + printf "\033[H\033[2K%s Scanning applications... %d/%d\n" "$c" "$completed" "$total_apps" >&2 else - echo -ne "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2 + printf "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2 fi ((i++)) sleep 0.1 2> /dev/null || sleep 1 @@ -346,12 +313,30 @@ scan_applications() { fi # Sort by last used (oldest first) and cache the result + # Show brief processing message for large app lists + if [[ $total_apps -gt 50 ]]; then + if [[ $inline_loading == true ]]; then + printf "\033[H\033[2KProcessing %d applications...\n" "$total_apps" >&2 + else + printf "\rProcessing %d applications... " "$total_apps" >&2 + fi + fi + sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { rm -f "$temp_file" return 1 } rm -f "$temp_file" + # Clear processing message + if [[ $total_apps -gt 50 ]]; then + if [[ $inline_loading == true ]]; then + printf "\033[H\033[2K" >&2 + else + printf "\r\033[K" >&2 + fi + fi + # Save to cache (simplified - no metadata) cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true @@ -555,18 +540,22 @@ main() { # Show selected apps with clean alignment echo -e "${BLUE}${ICON_CONFIRM}${NC} Selected ${selection_count} app(s):" local -a summary_rows=() - local max_name_width=0 + local max_name_display_width=0 local max_size_width=0 local name_trunc_limit=30 for selected_app in "${selected_apps[@]}"; do IFS='|' read -r epoch app_path app_name bundle_id size last_used size_kb <<< "$selected_app" - local display_name="$app_name" - if [[ ${#display_name} -gt $name_trunc_limit ]]; then - display_name="${display_name:0:$((name_trunc_limit - 3))}..." - fi - [[ ${#display_name} -gt $max_name_width ]] && max_name_width=${#display_name} + # Truncate by display width if needed + local display_name + display_name=$(truncate_by_display_width "$app_name" "$name_trunc_limit") + + # Get actual display width + local current_width + current_width=$(get_display_width "$display_name") + + [[ $current_width -gt $max_name_display_width ]] && max_name_display_width=$current_width local size_display="$size" if [[ -z "$size_display" || "$size_display" == "0" || "$size_display" == "N/A" ]]; then @@ -580,13 +569,20 @@ main() { summary_rows+=("$display_name|$size_display|$last_display") done - ((max_name_width < 16)) && max_name_width=16 + ((max_name_display_width < 16)) && max_name_display_width=16 ((max_size_width < 5)) && max_size_width=5 local index=1 for row in "${summary_rows[@]}"; do IFS='|' read -r name_cell size_cell last_cell <<< "$row" - printf "%d. %-*s %*s | Last: %s\n" "$index" "$max_name_width" "$name_cell" "$max_size_width" "$size_cell" "$last_cell" + # Calculate printf width based on actual display width + local name_display_width + name_display_width=$(get_display_width "$name_cell") + local name_char_count=${#name_cell} + local padding_needed=$((max_name_display_width - name_display_width)) + local printf_name_width=$((name_char_count + padding_needed)) + + printf "%d. %-*s %*s | Last: %s\n" "$index" "$printf_name_width" "$name_cell" "$max_size_width" "$size_cell" "$last_cell" ((index++)) done diff --git a/bin/uninstall_lib.sh b/bin/uninstall_lib.sh index 998ba1e..6ec88f2 100755 --- a/bin/uninstall_lib.sh +++ b/bin/uninstall_lib.sh @@ -88,7 +88,13 @@ scan_applications() { fi fi - # Cache miss - show scanning feedback below + # Cache miss - prepare for scanning + local inline_loading=false + if [[ -t 1 && -t 2 ]]; then + inline_loading=true + # Clear screen for inline loading + printf "\033[2J\033[H" >&2 + fi local temp_file temp_file=$(create_temp_file) @@ -97,11 +103,7 @@ scan_applications() { local current_epoch current_epoch=$(date "+%s") - # Spinner for scanning feedback (simple ASCII for compatibility) - local spinner_chars="|/-\\" - local spinner_idx=0 - - # First pass: quickly collect all valid app paths and bundle IDs + # First pass: quickly collect all valid app paths and bundle IDs (NO mdls calls) local -a app_data_tuples=() while IFS= read -r -d '' app_path; do if [[ ! -e "$app_path" ]]; then continue; fi @@ -118,78 +120,19 @@ scan_applications() { continue fi - # Try to get English name from bundle info, fallback to folder name + # Get bundle ID only (fast, no mdls calls in first pass) local bundle_id="unknown" - local display_name="$app_name" if [[ -f "$app_path/Contents/Info.plist" ]]; then bundle_id=$(defaults read "$app_path/Contents/Info.plist" CFBundleIdentifier 2> /dev/null || echo "unknown") - - # Try to get English name from bundle info - local bundle_executable - bundle_executable=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null) - - # Smart display name selection - prefer descriptive names over generic ones - local candidates=() - - # Get all potential names - local bundle_display_name - bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null) - local bundle_name - bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null) - - # Check if executable name is generic/technical (should be avoided) - local is_generic_executable=false - if [[ -n "$bundle_executable" ]]; then - case "$bundle_executable" in - "pake" | "Electron" | "electron" | "nwjs" | "node" | "helper" | "main" | "app" | "binary") - is_generic_executable=true - ;; - esac - fi - - # Priority order for name selection: - # 1. App folder name (if ASCII and descriptive) - often the most complete name - if [[ "$app_name" =~ ^[A-Za-z0-9\ ._-]+$ && ${#app_name} -gt 3 ]]; then - candidates+=("$app_name") - fi - - # 2. CFBundleDisplayName (if meaningful and ASCII) - if [[ -n "$bundle_display_name" && "$bundle_display_name" =~ ^[A-Za-z0-9\ ._-]+$ ]]; then - candidates+=("$bundle_display_name") - fi - - # 3. CFBundleName (if meaningful and ASCII) - if [[ -n "$bundle_name" && "$bundle_name" =~ ^[A-Za-z0-9\ ._-]+$ && "$bundle_name" != "$bundle_display_name" ]]; then - candidates+=("$bundle_name") - fi - - # 4. CFBundleExecutable (only if not generic and ASCII) - if [[ -n "$bundle_executable" && "$bundle_executable" =~ ^[A-Za-z0-9._-]+$ && "$is_generic_executable" == false ]]; then - candidates+=("$bundle_executable") - fi - - # 5. Fallback to non-ASCII names if no ASCII found - if [[ ${#candidates[@]} -eq 0 ]]; then - [[ -n "$bundle_display_name" ]] && candidates+=("$bundle_display_name") - [[ -n "$bundle_name" && "$bundle_name" != "$bundle_display_name" ]] && candidates+=("$bundle_name") - candidates+=("$app_name") - fi - - # Select the first (best) candidate - display_name="${candidates[0]:-$app_name}" - - # Apply brand name mapping from common.sh - display_name="$(get_brand_name "$display_name")" fi # Skip system critical apps (input methods, system components) - # Note: Paid apps like CleanMyMac, 1Password are NOT protected here - users can uninstall them if should_protect_from_uninstall "$bundle_id"; then continue fi - # Store tuple: app_path|app_name|bundle_id|display_name - app_data_tuples+=("${app_path}|${app_name}|${bundle_id}|${display_name}") + # Store tuple: app_path|app_name|bundle_id (display_name will be resolved in parallel later) + app_data_tuples+=("${app_path}|${app_name}|${bundle_id}") done < <( # Scan both system and user application directories # Using maxdepth 3 to find apps in subdirectories (e.g., Adobe apps in /Applications/Adobe X/) @@ -209,11 +152,7 @@ scan_applications() { max_parallel=32 fi local pids=() - local inline_loading=false - if [[ "${MOLE_INLINE_LOADING:-}" == "1" || "${MOLE_INLINE_LOADING:-}" == "true" ]]; then - inline_loading=true - printf "\033[H" >&2 # Position cursor at top of screen - fi + # inline_loading variable already set above (line ~92) # Process app metadata extraction function process_app_metadata() { @@ -221,7 +160,35 @@ scan_applications() { local output_file="$2" local current_epoch="$3" - IFS='|' read -r app_path app_name bundle_id display_name <<< "$app_data_tuple" + IFS='|' read -r app_path app_name bundle_id <<< "$app_data_tuple" + + # Get localized display name (moved from first pass for better performance) + local display_name="$app_name" + if [[ -f "$app_path/Contents/Info.plist" ]]; then + # Try to get localized name from system metadata (best for i18n) + local md_display_name + md_display_name=$(run_with_timeout 0.05 mdls -name kMDItemDisplayName -raw "$app_path" 2> /dev/null || echo "") + + # Get bundle names + local bundle_display_name + bundle_display_name=$(plutil -extract CFBundleDisplayName raw "$app_path/Contents/Info.plist" 2> /dev/null) + local bundle_name + bundle_name=$(plutil -extract CFBundleName raw "$app_path/Contents/Info.plist" 2> /dev/null) + + # Priority order for name selection (prefer localized names): + # 1. System metadata display name (kMDItemDisplayName) - respects system language + # 2. CFBundleDisplayName - usually localized + # 3. CFBundleName - fallback + # 4. App folder name - last resort + + if [[ -n "$md_display_name" && "$md_display_name" != "(null)" && "$md_display_name" != "$app_name" ]]; then + display_name="$md_display_name" + elif [[ -n "$bundle_display_name" && "$bundle_display_name" != "(null)" ]]; then + display_name="$bundle_display_name" + elif [[ -n "$bundle_name" && "$bundle_name" != "(null)" ]]; then + display_name="$bundle_name" + fi + fi # Parallel size calculation local app_size="N/A" @@ -293,9 +260,9 @@ scan_applications() { local completed=$(cat "$progress_file" 2> /dev/null || echo 0) local c="${spinner_chars:$((i % 4)):1}" if [[ $inline_loading == true ]]; then - printf "\033[H\033[2K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2 + printf "\033[H\033[2K%s Scanning applications... %d/%d\n" "$c" "$completed" "$total_apps" >&2 else - echo -ne "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2 + printf "\r\033[K%s Scanning applications... %d/%d" "$c" "$completed" "$total_apps" >&2 fi ((i++)) sleep 0.1 2> /dev/null || sleep 1 @@ -346,12 +313,30 @@ scan_applications() { fi # Sort by last used (oldest first) and cache the result + # Show brief processing message for large app lists + if [[ $total_apps -gt 50 ]]; then + if [[ $inline_loading == true ]]; then + printf "\033[H\033[2KProcessing %d applications...\n" "$total_apps" >&2 + else + printf "\rProcessing %d applications... " "$total_apps" >&2 + fi + fi + sort -t'|' -k1,1n "$temp_file" > "${temp_file}.sorted" || { rm -f "$temp_file" return 1 } rm -f "$temp_file" + # Clear processing message + if [[ $total_apps -gt 50 ]]; then + if [[ $inline_loading == true ]]; then + printf "\033[H\033[2K" >&2 + else + printf "\r\033[K" >&2 + fi + fi + # Save to cache (simplified - no metadata) cp "${temp_file}.sorted" "$cache_file" 2> /dev/null || true diff --git a/lib/ui/app_selector.sh b/lib/ui/app_selector.sh index 7454b6b..7d402d4 100755 --- a/lib/ui/app_selector.sh +++ b/lib/ui/app_selector.sh @@ -3,6 +3,8 @@ set -euo pipefail +# Note: get_display_width() is now defined in lib/core/ui.sh + # Format app info for display format_app_display() { local display_name="$1" size="$2" last_used="$3" @@ -20,18 +22,26 @@ format_app_display() { local fixed_width=28 local available_width=$((terminal_width - fixed_width)) - # Set reasonable bounds for name width: 24-35 chars + # Set reasonable bounds for name width: 24-35 display width [[ $available_width -lt 24 ]] && available_width=24 [[ $available_width -gt 35 ]] && available_width=35 - # Truncate long names if needed - local truncated_name="$display_name" - if [[ ${#display_name} -gt $available_width ]]; then - truncated_name="${display_name:0:$((available_width - 3))}..." - fi + # Truncate long names if needed (based on display width, not char count) + local truncated_name + truncated_name=$(truncate_by_display_width "$display_name" "$available_width") - # Use dynamic column width for better readability - printf "%-*s %9s | %s" "$available_width" "$truncated_name" "$size_str" "$compact_last_used" + # Get actual display width after truncation + local current_display_width + current_display_width=$(get_display_width "$truncated_name") + + # Calculate padding needed + # Formula: char_count + (available_width - display_width) = padding to add + local char_count=${#truncated_name} + local padding_needed=$((available_width - current_display_width)) + local printf_width=$((char_count + padding_needed)) + + # Use dynamic column width with corrected padding + printf "%-*s %9s | %s" "$printf_width" "$truncated_name" "$size_str" "$compact_last_used" } # Global variable to store selection result (bash 3.2 compatible) @@ -46,6 +56,14 @@ select_apps_for_uninstall() { fi # Build menu options + # Show loading for large lists (formatting can be slow due to width calculations) + local app_count=${#apps_data[@]} + if [[ $app_count -gt 30 ]]; then + if [[ -t 2 ]]; then + printf "\rPreparing %d applications... " "$app_count" >&2 + fi + fi + local -a menu_options=() # Prepare metadata (comma-separated) for sorting/filtering inside the menu local epochs_csv="" @@ -66,6 +84,13 @@ select_apps_for_uninstall() { ((idx++)) done + # Clear loading message + if [[ $app_count -gt 30 ]]; then + if [[ -t 2 ]]; then + printf "\r\033[K" >&2 + fi + fi + # Expose metadata for the paginated menu (optional inputs) # - MOLE_MENU_META_EPOCHS: numeric last_used_epoch per item # - MOLE_MENU_META_SIZEKB: numeric size in KB per item From be1027f9c347c84f3dffbd21a15973b384b4fa2a Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 10:37:03 +0800 Subject: [PATCH 10/18] refine sudo requirement checks for uninstallation. --- lib/core/base.sh | 62 ++++++++++++----- lib/core/sudo.sh | 2 +- lib/core/ui.sh | 152 +++++++++++++++++++++++++++++++++++++++++ lib/uninstall/batch.sh | 9 ++- mole | 4 +- 5 files changed, 206 insertions(+), 23 deletions(-) diff --git a/lib/core/base.sh b/lib/core/base.sh index eae2e80..5b4e236 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -246,27 +246,53 @@ bytes_to_human_kb() { # Get brand-friendly name for an application # Args: $1 - application name -# Returns: branded name if mapping exists, original name otherwise +# Returns: localized name based on system language preference get_brand_name() { local name="$1" - case "$name" in - "qiyimac" | "爱奇艺") echo "iQiyi" ;; - "wechat" | "微信") echo "WeChat" ;; - "QQ") echo "QQ" ;; - "VooV Meeting" | "腾讯会议") echo "VooV Meeting" ;; - "dingtalk" | "钉钉") echo "DingTalk" ;; - "NeteaseMusic" | "网易云音乐") echo "NetEase Music" ;; - "BaiduNetdisk" | "百度网盘") echo "Baidu NetDisk" ;; - "alipay" | "支付宝") echo "Alipay" ;; - "taobao" | "淘宝") echo "Taobao" ;; - "futunn" | "富途牛牛") echo "Futu NiuNiu" ;; - "tencent lemon" | "Tencent Lemon Cleaner") echo "Tencent Lemon" ;; - "keynote" | "Keynote") echo "Keynote" ;; - "pages" | "Pages") echo "Pages" ;; - "numbers" | "Numbers") echo "Numbers" ;; - *) echo "$name" ;; - esac + # Detect if system primary language is Chinese + local is_chinese=false + local sys_lang + sys_lang=$(defaults read -g AppleLanguages 2>/dev/null | grep -o 'zh-Hans\|zh-Hant\|zh' | head -1 || echo "") + [[ -n "$sys_lang" ]] && is_chinese=true + + # Return localized names based on system language + if [[ "$is_chinese" == true ]]; then + # Chinese system - prefer Chinese names + case "$name" in + "qiyimac" | "iQiyi") echo "爱奇艺" ;; + "wechat" | "WeChat") echo "微信" ;; + "QQ") echo "QQ" ;; + "VooV Meeting") echo "腾讯会议" ;; + "dingtalk" | "DingTalk") echo "钉钉" ;; + "NeteaseMusic" | "NetEase Music") echo "网易云音乐" ;; + "BaiduNetdisk" | "Baidu NetDisk") echo "百度网盘" ;; + "alipay" | "Alipay") echo "支付宝" ;; + "taobao" | "Taobao") echo "淘宝" ;; + "futunn" | "Futu NiuNiu") echo "富途牛牛" ;; + "tencent lemon" | "Tencent Lemon Cleaner" | "Tencent Lemon") echo "腾讯柠檬清理" ;; + *) echo "$name" ;; + esac + else + # Non-Chinese system - use English names + case "$name" in + "qiyimac" | "爱奇艺") echo "iQiyi" ;; + "wechat" | "微信") echo "WeChat" ;; + "QQ") echo "QQ" ;; + "腾讯会议") echo "VooV Meeting" ;; + "dingtalk" | "钉钉") echo "DingTalk" ;; + "网易云音乐") echo "NetEase Music" ;; + "百度网盘") echo "Baidu NetDisk" ;; + "alipay" | "支付宝") echo "Alipay" ;; + "taobao" | "淘宝") echo "Taobao" ;; + "富途牛牛") echo "Futu NiuNiu" ;; + "腾讯柠檬清理" | "Tencent Lemon Cleaner") echo "Tencent Lemon" ;; + "keynote" | "Keynote") echo "Keynote" ;; + "pages" | "Pages") echo "Pages" ;; + "numbers" | "Numbers") echo "Numbers" ;; + *) echo "$name" ;; + esac + fi } # ============================================================================ diff --git a/lib/core/sudo.sh b/lib/core/sudo.sh index 617dcaf..d44b719 100644 --- a/lib/core/sudo.sh +++ b/lib/core/sudo.sh @@ -45,7 +45,7 @@ _request_password() { # Save original terminal settings and ensure they're restored on exit local stty_orig stty_orig=$(stty -g < "$tty_path" 2> /dev/null || echo "") - trap '[[ -n "$stty_orig" ]] && stty "$stty_orig" < "$tty_path" 2> /dev/null || true' RETURN + trap '[[ -n "${stty_orig:-}" ]] && stty "${stty_orig:-}" < "$tty_path" 2> /dev/null || true' RETURN while ((attempts < 3)); do local password="" diff --git a/lib/core/ui.sh b/lib/core/ui.sh index 562e0c8..b4bab88 100755 --- a/lib/core/ui.sh +++ b/lib/core/ui.sh @@ -17,6 +17,158 @@ clear_screen() { printf '\033[2J\033[H'; } hide_cursor() { [[ -t 1 ]] && printf '\033[?25l' >&2 || true; } show_cursor() { [[ -t 1 ]] && printf '\033[?25h' >&2 || true; } +# Calculate display width of a string (CJK characters count as 2) +# Args: $1 - string to measure +# Returns: display width +# Note: Works correctly even when LC_ALL=C is set +get_display_width() { + local str="$1" + + # Check Python availability once and cache the result + # Use Python for accurate width calculation if available (cached check) + if [[ -z "${MOLE_PYTHON_AVAILABLE:-}" ]]; then + if command -v python3 > /dev/null 2>&1; then + export MOLE_PYTHON_AVAILABLE=1 + else + export MOLE_PYTHON_AVAILABLE=0 + fi + fi + + if [[ "${MOLE_PYTHON_AVAILABLE:-0}" == "1" ]]; then + python3 -c " +import sys +import unicodedata + +s = sys.argv[1] +width = 0 +for char in s: + # East Asian Width property + ea_width = unicodedata.east_asian_width(char) + if ea_width in ('F', 'W'): # Fullwidth or Wide + width += 2 + else: + width += 1 +print(width) +" "$str" 2>/dev/null && return + fi + + # Fallback: Use wc with UTF-8 locale temporarily + local saved_lc_all="${LC_ALL:-}" + local saved_lang="${LANG:-}" + + export LC_ALL=en_US.UTF-8 + export LANG=en_US.UTF-8 + + local char_count byte_count width + char_count=$(printf '%s' "$str" | wc -m 2>/dev/null | tr -d ' ') + byte_count=$(printf '%s' "$str" | wc -c 2>/dev/null | tr -d ' ') + + # Restore locale + if [[ -n "$saved_lc_all" ]]; then + export LC_ALL="$saved_lc_all" + else + unset LC_ALL + fi + if [[ -n "$saved_lang" ]]; then + export LANG="$saved_lang" + else + unset LANG + fi + + # Estimate: if byte_count > char_count, we have multibyte chars + # Rough approximation: each multibyte char (CJK) is ~3 bytes and width 2 + # ASCII chars are 1 byte and width 1 + if [[ $byte_count -gt $char_count ]]; then + local multibyte_chars=$((byte_count - char_count)) + # Assume most multibyte chars are 2 bytes extra (3 bytes total for UTF-8 CJK) + local cjk_chars=$((multibyte_chars / 2)) + local ascii_chars=$((char_count - cjk_chars)) + width=$((ascii_chars + cjk_chars * 2)) + else + width=$char_count + fi + + echo "$width" +} + +# Truncate string by display width (handles CJK correctly) +# Args: $1 - string, $2 - max display width +# Returns: truncated string with "..." if needed +truncate_by_display_width() { + local str="$1" + local max_width="$2" + local current_width + current_width=$(get_display_width "$str") + + if [[ $current_width -le $max_width ]]; then + echo "$str" + return + fi + + # Use Python for accurate truncation if available (use cached check) + if [[ "${MOLE_PYTHON_AVAILABLE:-0}" == "1" ]]; then + python3 -c " +import sys +import unicodedata + +s = sys.argv[1] +max_w = int(sys.argv[2]) +result = '' +width = 0 + +for char in s: + ea_width = unicodedata.east_asian_width(char) + char_width = 2 if ea_width in ('F', 'W') else 1 + + if width + char_width + 3 > max_w: # +3 for '...' + break + + result += char + width += char_width + +print(result + '...') +" "$str" "$max_width" 2>/dev/null && return + fi + + # Fallback: Use UTF-8 locale for proper string handling + local saved_lc_all="${LC_ALL:-}" + local saved_lang="${LANG:-}" + export LC_ALL=en_US.UTF-8 + export LANG=en_US.UTF-8 + + local truncated="" + local width=0 + local i=0 + local char char_width + + while [[ $i -lt ${#str} ]]; do + char="${str:$i:1}" + char_width=$(get_display_width "$char") + + if ((width + char_width + 3 > max_width)); then + break + fi + + truncated+="$char" + ((width += char_width)) + ((i++)) + done + + # Restore locale + if [[ -n "$saved_lc_all" ]]; then + export LC_ALL="$saved_lc_all" + else + unset LC_ALL + fi + if [[ -n "$saved_lang" ]]; then + export LANG="$saved_lang" + else + unset LANG + fi + + echo "${truncated}..." +} + # Keyboard input - read single keypress read_key() { local key rest read_status diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh index f477ef8..3ce00ec 100755 --- a/lib/uninstall/batch.sh +++ b/lib/uninstall/batch.sh @@ -130,9 +130,10 @@ batch_uninstall_applications() { running_apps+=("$app_name") fi - # Check if app requires sudo to delete + # Check if app requires sudo to delete (either app bundle or system files) + local needs_sudo=false if [[ ! -w "$(dirname "$app_path")" ]] || [[ "$(get_file_owner "$app_path")" == "root" ]]; then - sudo_apps+=("$app_name") + needs_sudo=true fi # Calculate size for summary (including system files) @@ -150,6 +151,10 @@ batch_uninstall_applications() { # Check if system files require sudo # shellcheck disable=SC2128 if [[ -n "$system_files" ]]; then + needs_sudo=true + fi + + if [[ "$needs_sudo" == "true" ]]; then sudo_apps+=("$app_name") fi diff --git a/mole b/mole index da659e8..d19ee0c 100755 --- a/mole +++ b/mole @@ -22,8 +22,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" # Version info -VERSION="1.13.3" -MOLE_TAGLINE="can dig deep to clean your Mac." +VERSION="1.13.4" +MOLE_TAGLINE="Deep clean and optimize your Mac." # Check if Touch ID is already configured is_touchid_configured() { From 3053e05ce4442fae18d21f0175a0b6b30982fa81 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 02:37:33 +0000 Subject: [PATCH 11/18] chore: auto format code --- lib/core/base.sh | 2 +- lib/core/ui.sh | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/core/base.sh b/lib/core/base.sh index 5b4e236..4555732 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -253,7 +253,7 @@ get_brand_name() { # Detect if system primary language is Chinese local is_chinese=false local sys_lang - sys_lang=$(defaults read -g AppleLanguages 2>/dev/null | grep -o 'zh-Hans\|zh-Hant\|zh' | head -1 || echo "") + sys_lang=$(defaults read -g AppleLanguages 2> /dev/null | grep -o 'zh-Hans\|zh-Hant\|zh' | head -1 || echo "") [[ -n "$sys_lang" ]] && is_chinese=true # Return localized names based on system language diff --git a/lib/core/ui.sh b/lib/core/ui.sh index b4bab88..9908444 100755 --- a/lib/core/ui.sh +++ b/lib/core/ui.sh @@ -49,7 +49,7 @@ for char in s: else: width += 1 print(width) -" "$str" 2>/dev/null && return +" "$str" 2> /dev/null && return fi # Fallback: Use wc with UTF-8 locale temporarily @@ -60,8 +60,8 @@ print(width) export LANG=en_US.UTF-8 local char_count byte_count width - char_count=$(printf '%s' "$str" | wc -m 2>/dev/null | tr -d ' ') - byte_count=$(printf '%s' "$str" | wc -c 2>/dev/null | tr -d ' ') + char_count=$(printf '%s' "$str" | wc -m 2> /dev/null | tr -d ' ') + byte_count=$(printf '%s' "$str" | wc -c 2> /dev/null | tr -d ' ') # Restore locale if [[ -n "$saved_lc_all" ]]; then @@ -127,7 +127,7 @@ for char in s: width += char_width print(result + '...') -" "$str" "$max_width" 2>/dev/null && return +" "$str" "$max_width" 2> /dev/null && return fi # Fallback: Use UTF-8 locale for proper string handling From 27205c653d414aba9fa6be308f07e4c4cda559f2 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 11:01:15 +0800 Subject: [PATCH 12/18] feat: Boost UI performance with pure bash string width calculation and truncation, and add visual feedback for cache hits in uninstall scripts. --- bin/uninstall.sh | 6 ++ bin/uninstall_lib.sh | 6 ++ lib/core/ui.sh | 152 +++++++++++++++++------------------------ lib/ui/app_selector.sh | 10 +-- 4 files changed, 80 insertions(+), 94 deletions(-) diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 4794a49..ef07cc3 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -83,6 +83,12 @@ scan_applications() { [[ $cache_age -eq $(date +%s) ]] && cache_age=86401 # Handle missing file if [[ $cache_age -lt $cache_ttl ]]; then # Cache hit - return immediately + # Show brief flash of cache usage if in interactive mode + if [[ -t 2 ]]; then + echo -e "${GREEN}Loading from cache...${NC}" >&2 + # Small sleep to let user see it (optional, but good for "feeling" the speed vs glitch) + sleep 0.3 + fi echo "$cache_file" return 0 fi diff --git a/bin/uninstall_lib.sh b/bin/uninstall_lib.sh index 6ec88f2..7cc1926 100755 --- a/bin/uninstall_lib.sh +++ b/bin/uninstall_lib.sh @@ -83,6 +83,12 @@ scan_applications() { [[ $cache_age -eq $(date +%s) ]] && cache_age=86401 # Handle missing file if [[ $cache_age -lt $cache_ttl ]]; then # Cache hit - return immediately + # Show brief flash of cache usage if in interactive mode + if [[ -t 2 ]]; then + echo -e "${GREEN}Loading from cache...${NC}" >&2 + # Small sleep to let user see it (optional, but good for "feeling" the speed vs glitch) + sleep 0.3 + fi echo "$cache_file" return 0 fi diff --git a/lib/core/ui.sh b/lib/core/ui.sh index 9908444..c367a1f 100755 --- a/lib/core/ui.sh +++ b/lib/core/ui.sh @@ -24,76 +24,50 @@ show_cursor() { [[ -t 1 ]] && printf '\033[?25h' >&2 || true; } get_display_width() { local str="$1" - # Check Python availability once and cache the result - # Use Python for accurate width calculation if available (cached check) - if [[ -z "${MOLE_PYTHON_AVAILABLE:-}" ]]; then - if command -v python3 > /dev/null 2>&1; then - export MOLE_PYTHON_AVAILABLE=1 - else - export MOLE_PYTHON_AVAILABLE=0 - fi - fi + # Optimized pure bash implementation without forks + local width - if [[ "${MOLE_PYTHON_AVAILABLE:-0}" == "1" ]]; then - python3 -c " -import sys -import unicodedata - -s = sys.argv[1] -width = 0 -for char in s: - # East Asian Width property - ea_width = unicodedata.east_asian_width(char) - if ea_width in ('F', 'W'): # Fullwidth or Wide - width += 2 - else: - width += 1 -print(width) -" "$str" 2> /dev/null && return - fi - - # Fallback: Use wc with UTF-8 locale temporarily - local saved_lc_all="${LC_ALL:-}" - local saved_lang="${LANG:-}" + # Save current locale + local old_lc="${LC_ALL:-}" + # Get Char Count (UTF-8) + # We must export ensuring it applies to the expansion (though just assignment often works in newer bash, export is safer for all subshells/cmds) export LC_ALL=en_US.UTF-8 - export LANG=en_US.UTF-8 + local char_count=${#str} - local char_count byte_count width - char_count=$(printf '%s' "$str" | wc -m 2> /dev/null | tr -d ' ') - byte_count=$(printf '%s' "$str" | wc -c 2> /dev/null | tr -d ' ') + # Get Byte Count (C) + export LC_ALL=C + local byte_count=${#str} - # Restore locale - if [[ -n "$saved_lc_all" ]]; then - export LC_ALL="$saved_lc_all" + # Restore Locale immediately + if [[ -n "$old_lc" ]]; then + export LC_ALL="$old_lc" else unset LC_ALL fi - if [[ -n "$saved_lang" ]]; then - export LANG="$saved_lang" - else - unset LANG + + if [[ $byte_count -eq $char_count ]]; then + echo "$char_count" + return fi - # Estimate: if byte_count > char_count, we have multibyte chars - # Rough approximation: each multibyte char (CJK) is ~3 bytes and width 2 - # ASCII chars are 1 byte and width 1 - if [[ $byte_count -gt $char_count ]]; then - local multibyte_chars=$((byte_count - char_count)) - # Assume most multibyte chars are 2 bytes extra (3 bytes total for UTF-8 CJK) - local cjk_chars=$((multibyte_chars / 2)) - local ascii_chars=$((char_count - cjk_chars)) - width=$((ascii_chars + cjk_chars * 2)) - else - width=$char_count - fi + # CJK Heuristic: + # Most CJK chars are 3 bytes in UTF-8 and width 2. + # ASCII chars are 1 byte and width 1. + # Width ~= CharCount + (ByteCount - CharCount) / 2 + # "中" (1 char, 3 bytes) -> 1 + (2)/2 = 2. + # "A" (1 char, 1 byte) -> 1 + 0 = 1. + # This is an approximation but very fast and sufficient for App names. + # Integer arithmetic in bash automatically handles floor. + local extra_bytes=$((byte_count - char_count)) + local padding=$((extra_bytes / 2)) + width=$((char_count + padding)) echo "$width" } # Truncate string by display width (handles CJK correctly) # Args: $1 - string, $2 - max display width -# Returns: truncated string with "..." if needed truncate_by_display_width() { local str="$1" local max_width="$2" @@ -105,45 +79,48 @@ truncate_by_display_width() { return fi - # Use Python for accurate truncation if available (use cached check) - if [[ "${MOLE_PYTHON_AVAILABLE:-0}" == "1" ]]; then - python3 -c " -import sys -import unicodedata -s = sys.argv[1] -max_w = int(sys.argv[2]) -result = '' -width = 0 + # Fallback: Use pure bash character iteration + # Since we need to know the width of *each* character to truncate at the right spot, + # we cannot just use the total width formula on the whole string. + # However, iterating char-by-char and calling the optimized get_display_width function + # is now much faster because it doesn't fork 'wc'. -for char in s: - ea_width = unicodedata.east_asian_width(char) - char_width = 2 if ea_width in ('F', 'W') else 1 - - if width + char_width + 3 > max_w: # +3 for '...' - break - - result += char - width += char_width - -print(result + '...') -" "$str" "$max_width" 2> /dev/null && return - fi - - # Fallback: Use UTF-8 locale for proper string handling - local saved_lc_all="${LC_ALL:-}" - local saved_lang="${LANG:-}" + # CRITICAL: Switch to UTF-8 for correct character iteration + local old_lc="${LC_ALL:-}" export LC_ALL=en_US.UTF-8 - export LANG=en_US.UTF-8 local truncated="" local width=0 local i=0 local char char_width + local strlen=${#str} # Re-calculate in UTF-8 - while [[ $i -lt ${#str} ]]; do + # Optimization: If total width <= max_width, return original string (checked above) + + while [[ $i -lt $strlen ]]; do char="${str:$i:1}" - char_width=$(get_display_width "$char") + + # Inlined width calculation for minimal overhead to avoid recursion overhead + # We are already in UTF-8, so ${#char} is char length (1). + # We need byte length for the heuristic. + # But switching locale inside loop is disastrous for perf. + # Logic: If char is ASCII (1 byte), width 1. + # If char is wide (3 bytes), width 2. + # How to detect byte size without switching locale? + # printf %s "$char" | wc -c ? Slow. + # Check against ASCII range? + # Fast ASCII check: if [[ "$char" < $'\x7f' ]]; then ... + + if [[ "$char" =~ [[:ascii:]] ]]; then + char_width=1 + else + # Assume wide for non-ascii in this context (simplified) + # Or use LC_ALL=C inside? No. + # Most non-ASCII in filenames are either CJK (width 2) or heavy symbols. + # Let's assume 2 for simplicity in this fast loop as we know we are usually dealing with CJK. + char_width=2 + fi if ((width + char_width + 3 > max_width)); then break @@ -155,16 +132,11 @@ print(result + '...') done # Restore locale - if [[ -n "$saved_lc_all" ]]; then - export LC_ALL="$saved_lc_all" + if [[ -n "$old_lc" ]]; then + export LC_ALL="$old_lc" else unset LC_ALL fi - if [[ -n "$saved_lang" ]]; then - export LANG="$saved_lang" - else - unset LANG - fi echo "${truncated}..." } diff --git a/lib/ui/app_selector.sh b/lib/ui/app_selector.sh index 7d402d4..49f8638 100755 --- a/lib/ui/app_selector.sh +++ b/lib/ui/app_selector.sh @@ -18,7 +18,8 @@ format_app_display() { [[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]] && size_str="$size" # Calculate available width for app name based on terminal width - local terminal_width=$(tput cols 2> /dev/null || echo 80) + # use passed width or calculate it (but calculation is slow in loops) + local terminal_width="${4:-$(tput cols 2> /dev/null || echo 80)}" local fixed_width=28 local available_width=$((terminal_width - fixed_width)) @@ -58,7 +59,8 @@ select_apps_for_uninstall() { # Build menu options # Show loading for large lists (formatting can be slow due to width calculations) local app_count=${#apps_data[@]} - if [[ $app_count -gt 30 ]]; then + local terminal_width=$(tput cols 2> /dev/null || echo 80) + if [[ $app_count -gt 100 ]]; then if [[ -t 2 ]]; then printf "\rPreparing %d applications... " "$app_count" >&2 fi @@ -72,7 +74,7 @@ select_apps_for_uninstall() { for app_data in "${apps_data[@]}"; do # Keep extended field 7 (size_kb) if present IFS='|' read -r epoch _ display_name _ size last_used size_kb <<< "$app_data" - menu_options+=("$(format_app_display "$display_name" "$size" "$last_used")") + menu_options+=("$(format_app_display "$display_name" "$size" "$last_used" "$terminal_width")") # Build csv lists (avoid trailing commas) if [[ $idx -eq 0 ]]; then epochs_csv="${epoch:-0}" @@ -85,7 +87,7 @@ select_apps_for_uninstall() { done # Clear loading message - if [[ $app_count -gt 30 ]]; then + if [[ $app_count -gt 100 ]]; then if [[ -t 2 ]]; then printf "\r\033[K" >&2 fi From ba8bc3dc8f4abed6c0fe32a9ae899c52a88e872b Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 03:02:06 +0000 Subject: [PATCH 13/18] chore: auto format code --- bin/uninstall.sh | 6 +++--- bin/uninstall_lib.sh | 6 +++--- lib/core/ui.sh | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bin/uninstall.sh b/bin/uninstall.sh index ef07cc3..1db3a32 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -85,9 +85,9 @@ scan_applications() { # Cache hit - return immediately # Show brief flash of cache usage if in interactive mode if [[ -t 2 ]]; then - echo -e "${GREEN}Loading from cache...${NC}" >&2 - # Small sleep to let user see it (optional, but good for "feeling" the speed vs glitch) - sleep 0.3 + echo -e "${GREEN}Loading from cache...${NC}" >&2 + # Small sleep to let user see it (optional, but good for "feeling" the speed vs glitch) + sleep 0.3 fi echo "$cache_file" return 0 diff --git a/bin/uninstall_lib.sh b/bin/uninstall_lib.sh index 7cc1926..b0c9f61 100755 --- a/bin/uninstall_lib.sh +++ b/bin/uninstall_lib.sh @@ -85,9 +85,9 @@ scan_applications() { # Cache hit - return immediately # Show brief flash of cache usage if in interactive mode if [[ -t 2 ]]; then - echo -e "${GREEN}Loading from cache...${NC}" >&2 - # Small sleep to let user see it (optional, but good for "feeling" the speed vs glitch) - sleep 0.3 + echo -e "${GREEN}Loading from cache...${NC}" >&2 + # Small sleep to let user see it (optional, but good for "feeling" the speed vs glitch) + sleep 0.3 fi echo "$cache_file" return 0 diff --git a/lib/core/ui.sh b/lib/core/ui.sh index c367a1f..66ecd68 100755 --- a/lib/core/ui.sh +++ b/lib/core/ui.sh @@ -79,7 +79,6 @@ truncate_by_display_width() { return fi - # Fallback: Use pure bash character iteration # Since we need to know the width of *each* character to truncate at the right spot, # we cannot just use the total width formula on the whole string. From eff0d1600bc8c4a203c981b7b50537bd0d9aea36 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 03:02:25 +0000 Subject: [PATCH 14/18] chore: update binaries for V1.13.4 --- bin/analyze-go | Bin 7298178 -> 7298178 bytes bin/status-go | Bin 7422050 -> 7422050 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/analyze-go b/bin/analyze-go index 2c5f688768499c0893a7f20da568895ae09a8341..c257f823b3cff1f3e8919064e843e8f2c47d154e 100755 GIT binary patch delta 1238 zcmcK2UrZAR90%|?Cm;^(P$#ULUSTRu;r_ID*P_f;XhA6STG~=hS-IZzCM|`A@<)kT zPK{}!%d+W~ZXp|4W)zGj;I^DfoRh_vuqWJ1SeCeK!HhsM_MizPOW1dBKKQoy@Ok+? z{PN8=-;+0Ptv(53)*^#@`tnz%8usw7OXahVn90I%`g-m;!qDOh1Z%zht|&zdM_4DX z@fTU@^SE5mP$Uv}2o{xzks)h~;!Y$jN-Dy+ z5>hnm>5)tFiIwvwh~iTkk=BS+@B*xc7hw(LKrZCLTF8fW@Ddcjde{I4co_;|BW!}r z@Cs~!A}EGeb&W87ma~zk%oI(tdAr_7ZUT}iW9M@HS8UaT;}Yw$X3g>6892}+<8-hl0}19n0g z?1DF8H|&9OsL&TH$WO%p`vSiuIBuow106}$!e!43{M z0M$?fPH;gj)WLs$>Y)ML&_SerWjM zi}DGw;8JQPZN4cX}4BJ44-h`s(sXU(OY_rp_~<0ZoWHl`|FSH UaHHQ0KcITQ{ZXH6=>E>|FDApp3IG5A delta 1242 zcmcK2TWm`K90u?hHs)4lv$kx<<}zkG=k%PmXB*b)bX#wyw`td5TibKi%hsB0E>CJC zU5NXoh{fyyL0lf>gk*xmA})y#Aqeq6mY6h%L`d*?^WaT9{PIu!PhY-#y;q0E_QEAD zf7+>@s^imhs^6qv-9Dknewf<&O(DOm8_3#E$UK6VsU-p-1u2pMV__VOhfK(V2`~||VG>M+DKHhLfefa@49J0*kPCS*3-Un@ z3MoM-`!jPiG(|BqTjmc6tUzm8StcM+3zf}TC4YSA3Xb+`1cni4EyHkXZ9rs16~_cQ zt(r;|^ojCpjhm(_DW;NUQ~EwJH<>P1!fcoW1yBeC%!MK-hIud_7QjL%fkm(wN?{3< zLAjJJCy!qKUujcFD^IQK6Vc=d2^7$v0tQ%6gGNe>X!Dz8E&UsZZ;viN{^M)a%a(X7 zE}g06!V{e4xZ7rmI+``L;izeI+!?jINMD#~sIdCnLDtCAT`G&m8WYJ5p^$9sGsTbU)qdmI)OjW3Lp#fHb2fWY-K4^l~um;vb zGqgY}_#ps65TrV-sH^`EX_MG3v_S~M5P>MPLx-gAi@gzb+iOP6`#uzWIenxn5jeSL z+||2l`;A)BOCM7|T|Crlew5ey+7iA$qg3%Ihrg+JMfNEVS3Q4DQt!{E2j4xJDEZT| z1fv_XQyP>#D;?d`pxm-Qa|i2qchJ>1|M8@X@8ZSE;bKdkLd*!B=RQY9F6_Q{#Wuv- zx9sg1xO43Kwd&!wh1+aWQE@y`LcFhgu9j^B{|0<*}#r~m)} diff --git a/bin/status-go b/bin/status-go index 5bf282402f16bffc6f891263976c8e740ea84d82..5a084efb33ef67b6e75ee48dcbf707e4b73ba4a1 100755 GIT binary patch delta 1173 zcmcK2TS!v@9LI6Y)YNjzOL^&XvYTnTv+W$uW*6JVZJN%xqi)kukLPUDxjA>SObRv= zhFu;s$e@R!Ff0hIz|&h{h+STc!Z4voBJ82J3Zw`7K`%Y@6!`FY`9J;m^IeL}#4o|5 zDqE_$dF1tvF_q%kL}+dk`SIQZ`QxM!LTc-FG9h)ZVD(x!n@<<24OjVFWt=9=EBl;h zY$mg@f%SG+^v9cI8k@mg+(`TBnucC)S3n!^_XlMaO$V^?!_OMVKm zdL>HaoR1K(2oVSCARg930wh8bY=C4)fmGNCn;;D&Acf744jGUMS+E7RLN??;t{5Tm z&c_}&>0$+=AZfmoRtl6T$0{ZIf0pb(1SAQVFh z$V77?sXn!4ixEb-Ko(h?o_PMR0FlFiW=>2Y;*VxLlvIfD@dAi?Ck`AWQsMI*T zX1!h36Q~Zb)uwip(X0*nXe(tp-5KLk)lde9;4mD4qfidVKm%IPfgUQr07fuDB~-z2 zsD>IaLoHaK4y+=d%B~3ie5=2Qo&En1Vh921p#d7f26k{j6EwpKI0-G#3T?naJMbWY z6I|dHqkSET&(~n=pK2FPov}re2fDxuKIjI&xM=E`jS)hoLwIzNN}qo5P}gza^1UuM z`ex|*tG4vTOXSp^feCSXuIDL5lq6jq{yF#75G1cw>Bk18W8-IrCP&J7LUL#D%e?Sj wyp<4$6cK5L;`uDqr{=K__s-pT^77kUSebR(KJ__6v(i%kE4nIbS{4xt~5v37YGBNiyC(3qL@Z#F!g)u z!)_I0H!J$AYH6cL(OlyeS|kC9);!?zhLoW|tXJgii_vCfc_^akv9T5tF=zq}1sT-p zjUg)UVw_6EDITnc6xaZ%kOt|n5jH^vWWr|1f-S%Y0c68g$boIJ9d^J@$b~%E#l@-o zbG#fWL(_y23++yq*u_XY#Kghz3-YJai~hPWa+a}6U4*z886m7h=IA7y z7B%K&NKFh~L6ZtbJTeqDiW0iweAo?pU@z6u}WFh7u?RA(VlL z(-qV5@R}u_Ptu9`Bm?C@KnxO)f{dF_vQ_PZdwa*7`AyNz_`db$eYg5)4aBmd1#0prNTtWOf?5A~82jTV=IsqOkgO z)h!|-4z+m9dXGotlDX|Zhq~KlNH{X(Pyt8b7*xV>sDcxq041D+YEVH9s6hj@PzUwU z09w$29)ACCgeK0BY5eZ|XRU#tapwPrkiZbY1ZJ>6GqiveTHzF&hBMFxHfVZL?A2fzUo+Tl x$mVXPhT;_&@n)y6*l^KfV1)UZYhJt1a^OivN10d;V+&r({AD{}%yWpX&es From e363a67a10b201cf434d73cd5996e011e5c67903 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 11:55:25 +0800 Subject: [PATCH 15/18] feat: optimize update checks for speed and refactor update flow to prioritize Mole with manual tips for other updates. --- .github/ISSUE_TEMPLATE/bug_report.md | 9 ++++---- README.md | 32 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e21a792..5ef3658 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -40,10 +40,11 @@ Paste the debug output here ## Environment -- Mole version: (run `mo --version`) -- macOS version: (run `sw_vers`) -- Installation method: (Homebrew / curl script) -- Architecture: (Intel / Apple Silicon) +Please run `mo update` to ensure you are on the latest version, then paste the output of `mo --version` below: + +```text +Paste mo --version output here +``` ## Additional context diff --git a/README.md b/README.md index ed59746..b526c70 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,15 @@ ## Features -- **All-in-one toolkit** equal to CleanMyMac + AppCleaner + DaisyDisk + Sensei + iStat in one **trusted binary** -- **Deep cleanup** finds and removes caches, temp files, browser leftovers, and junk to **free up tens of gigabytes** -- **Smart uninstall** finds app bundles plus launch agents, settings, caches, logs, and **leftover files** -- **Disk insight + optimization** show large files, display folders, **rebuild caches**, clean swap, refresh services -- **Live status** shows CPU, GPU, memory, disk, network, battery, and proxy data so you can **find problems** +- **All-in-one toolkit** combining the power of CleanMyMac, AppCleaner, DaisyDisk, Sensei, and iStat in one **trusted binary** +- **Deep cleanup** scans and removes caches, logs, browser leftovers, and junk to **reclaim tens of gigabytes** +- **Smart uninstall** completely removes apps including launch agents, preferences, caches, and **hidden leftovers** +- **Disk insight + optimization** visualizes usage, handles large files, **rebuilds caches**, cleans swap, and refreshes services +- **Live status** monitors CPU, GPU, memory, disk, network, battery, and proxy stats to **diagnose issues** ## Quick Start -**Install:** +**Installation:** ```bash curl -fsSL https://raw.githubusercontent.com/tw93/mole/main/install.sh | bash @@ -63,12 +63,12 @@ mo optimize --whitelist # Adjust protected optimization items ## Tips -- **Terminal**: iTerm2 has known compatibility issues, use Alacritty, kitty, WezTerm, Ghostty, or Warp instead -- **Safety**: Built with strict protections. See our [Security Audit](SECURITY_AUDIT.md). Preview with `mo clean --dry-run` -- **Whitelist**: Use `mo clean --whitelist` to manage protected caches -- **Touch ID**: Run `mo touchid` to approve sudo with Touch ID instead of password -- **Navigation**: All menus support Vim keys `h/j/k/l` in addition to arrow keys -- **Debug**: Use `--debug` flag to see detailed logs: `mo clean --debug` +- **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`. +- **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`). ## Features in Detail @@ -195,10 +195,10 @@ Adds 5 commands: `clean`, `uninstall`, `optimize`, `analyze`, `status`. Finds yo -- If Mole freed storage for you, consider starring the repo or sharing it with friends needing a cleaner Mac. -- Have ideas or fixes? Open an issue or PR and help shape Mole's future together with the community. -- Report bugs by running commands with `--debug` flag and sharing the output: `mo clean --debug` -- Love cats? Treat Tangyuan and Cola to canned food via this link and keep the mascots purring. +- If Mole saved you space, consider starring the repo or sharing it with friends who need a cleaner Mac. +- Have ideas or fixes? Open an issue or PR to help shape Mole's future with the community. + +- Love cats? Treat Tangyuan and Cola to canned food via this link to keep our mascots purring. ## License From a667a1a7771ce982e1f22475b81c720d12c2baea Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 11:56:39 +0800 Subject: [PATCH 16/18] feat: Bump version to 1.13.5, enhance `show_version` output, optimize software update checks, and add Touch ID for sudo as a security fix. --- bin/optimize.sh | 25 +++- bin/uninstall.sh | 24 ++-- lib/check/all.sh | 97 +++----------- lib/manage/update.sh | 293 +++++++---------------------------------- lib/uninstall/batch.sh | 7 +- mole | 41 +++++- 6 files changed, 138 insertions(+), 349 deletions(-) diff --git a/bin/optimize.sh b/bin/optimize.sh index 3f474c9..8215962 100755 --- a/bin/optimize.sh +++ b/bin/optimize.sh @@ -99,9 +99,11 @@ show_optimization_summary() { fi summary_details+=("$summary_line4") - if [[ "${OPTIMIZE_SHOW_TOUCHID_TIP:-false}" == "true" ]]; then - echo -e "${YELLOW}☻${NC} Run ${GRAY}mo touchid${NC} to approve sudo via Touch ID" + if [[ -n "${AUTO_FIX_SUMMARY:-}" ]]; then + summary_details+=("$AUTO_FIX_SUMMARY") fi + + # Fix: Ensure summary is always printed for optimizations print_summary_block "$summary_title" "${summary_details[@]}" } @@ -245,6 +247,11 @@ collect_security_fix_actions() { SECURITY_FIXES+=("gatekeeper|Enable Gatekeeper (App download protection)") fi fi + if touchid_supported && ! touchid_configured; then + if ! is_whitelisted "touchid"; then + SECURITY_FIXES+=("touchid|Enable Touch ID for sudo") + fi + fi ((${#SECURITY_FIXES[@]} > 0)) } @@ -301,6 +308,13 @@ apply_gatekeeper_fix() { return 1 } +apply_touchid_fix() { + if "$SCRIPT_DIR/bin/touchid.sh" enable; then + return 0 + fi + return 1 +} + perform_security_fixes() { if ! ensure_sudo_session "Security changes require admin access"; then echo -e "${YELLOW}${ICON_WARNING}${NC} Skipped security fixes (sudo denied)" @@ -317,6 +331,9 @@ perform_security_fixes() { gatekeeper) apply_gatekeeper_fix && ((applied++)) ;; + touchid) + apply_touchid_fix && ((applied++)) + ;; esac done @@ -496,10 +513,6 @@ main() { export OPTIMIZE_SAFE_COUNT=$safe_count export OPTIMIZE_CONFIRM_COUNT=$confirm_count - export OPTIMIZE_SHOW_TOUCHID_TIP="false" - if touchid_supported && ! touchid_configured; then - export OPTIMIZE_SHOW_TOUCHID_TIP="true" - fi # Show optimization summary at the end show_optimization_summary diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 1db3a32..7a4fbdb 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -599,22 +599,20 @@ main() { rm -f "$apps_file" # Pause before looping back - echo -e "${GRAY}Press Enter to return to application list, ESC to exit...${NC}" + echo -e "${GRAY}Press Enter to return to application list, any other key to exit...${NC}" local key IFS= read -r -s -n1 key || key="" - drain_pending_input # Clean up any escape sequence remnants - case "$key" in - $'\e' | q | Q) - show_cursor - return 0 - ;; - *) - # Continue loop - ;; - esac + drain_pending_input - # Reset force_rescan to false for subsequent loops, - # but relying on batch_uninstall's cache deletion for actual update + # Logic: Enter = continue loop, any other key = exit + if [[ -z "$key" ]]; then + : # Enter pressed, continue loop + else + show_cursor + return 0 + fi + + # Reset force_rescan to false for subsequent loops force_rescan=false done } diff --git a/lib/check/all.sh b/lib/check/all.sh index 09e2be2..6a453aa 100644 --- a/lib/check/all.sh +++ b/lib/check/all.sh @@ -275,98 +275,37 @@ SOFTWARE_UPDATE_LIST="" get_software_updates() { local cache_file="$CACHE_DIR/softwareupdate_list" - if [[ -z "$SOFTWARE_UPDATE_LIST" ]]; then - # Check cache first - if is_cache_valid "$cache_file"; then - SOFTWARE_UPDATE_LIST=$(cat "$cache_file" 2> /dev/null || echo "") - else - # Show spinner while checking (only on first call) - local show_spinner=false - if [[ -t 1 && -z "${SOFTWAREUPDATE_SPINNER_SHOWN:-}" ]]; then - start_inline_spinner "Checking system updates (querying Apple servers)..." - show_spinner=true - export SOFTWAREUPDATE_SPINNER_SHOWN="true" - fi + # Optimized: Use defaults to check if updates are pending (much faster) + local pending_updates + pending_updates=$(defaults read /Library/Preferences/com.apple.SoftwareUpdate LastRecommendedUpdatesAvailable 2> /dev/null || echo "0") - SOFTWARE_UPDATE_LIST=$(softwareupdate -l 2> /dev/null || echo "") - # Save to cache - echo "$SOFTWARE_UPDATE_LIST" > "$cache_file" 2> /dev/null || true - - # Stop spinner - if [[ "$show_spinner" == "true" ]]; then - stop_inline_spinner - fi - fi + if [[ "$pending_updates" -gt 0 ]]; then + echo "Updates Available" + else + echo "" fi - echo "$SOFTWARE_UPDATE_LIST" } check_appstore_updates() { - local spinner_started=false - if [[ -t 1 ]]; then - printf " Checking App Store updates...\r" - start_inline_spinner "Checking App Store updates (querying Apple servers)..." - spinner_started=true - export SOFTWAREUPDATE_SPINNER_SHOWN="external" - else - echo "Checking App Store updates..." - fi - - local update_list="" - update_list=$(get_software_updates | grep -v "Software Update Tool" | grep "^\*" | grep -vi "macOS" || echo "") - - if [[ "$spinner_started" == "true" ]]; then - stop_inline_spinner - unset SOFTWAREUPDATE_SPINNER_SHOWN - fi - - local update_count=0 - if [[ -n "$update_list" ]]; then - update_count=$(echo "$update_list" | wc -l | tr -d ' ') - fi - - export APPSTORE_UPDATE_COUNT=$update_count - - if [[ $update_count -gt 0 ]]; then - echo -e " ${YELLOW}${ICON_WARNING}${NC} App Store ${YELLOW}${update_count} apps${NC} need update" - echo -e " ${GRAY}updates available in final step${NC}" - else - echo -e " ${GREEN}✓${NC} App Store Up to date" - fi + # Skipped for speed optimization - consolidated into check_macos_update + # We can't easily distinguish app store vs macos updates without the slow softwareupdate -l call + export APPSTORE_UPDATE_COUNT=0 } check_macos_update() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_macos_updates"; then return; fi - local spinner_started=false - if [[ -t 1 ]]; then - printf " Checking macOS updates...\r" - start_inline_spinner "Checking macOS updates (querying Apple servers)..." - spinner_started=true - export SOFTWAREUPDATE_SPINNER_SHOWN="external" - else - echo "Checking macOS updates..." + + # Fast check using system preferences + local updates_available="false" + if [[ $(get_software_updates) == "Updates Available" ]]; then + updates_available="true" fi - # Check for macOS system update using cached list - local macos_update="" - macos_update=$(get_software_updates | grep -i "macOS" | head -1 || echo "") + export MACOS_UPDATE_AVAILABLE="$updates_available" - if [[ "$spinner_started" == "true" ]]; then - stop_inline_spinner - unset SOFTWAREUPDATE_SPINNER_SHOWN - fi - - export MACOS_UPDATE_AVAILABLE="false" - - if [[ -n "$macos_update" ]]; then - export MACOS_UPDATE_AVAILABLE="true" - local version=$(echo "$macos_update" | grep -o '[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?' | head -1) - if [[ -n "$version" ]]; then - echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}${version} available${NC}" - else - echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}Update available${NC}" - fi + if [[ "$updates_available" == "true" ]]; then + echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}Update available${NC}" echo -e " ${GRAY}update available in final step${NC}" else echo -e " ${GREEN}✓${NC} macOS Up to date" diff --git a/lib/manage/update.sh b/lib/manage/update.sh index b61fa0f..fd00fb6 100644 --- a/lib/manage/update.sh +++ b/lib/manage/update.sh @@ -76,174 +76,51 @@ ask_for_updates() { echo -e "$item" done echo "" - echo -ne "${YELLOW}Update all now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: " - local key - if ! key=$(read_key); then - echo "skip" - echo "" - return 1 + # If Mole has updates, offer to update it + if [[ "${MOLE_UPDATE_AVAILABLE:-}" == "true" ]]; then + echo -ne "${YELLOW}Update Mole now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: " + + local key + if ! key=$(read_key); then + echo "skip" + echo "" + return 1 + fi + + if [[ "$key" == "ENTER" ]]; then + echo "yes" + echo "" + return 0 + else + echo "skip" + echo "" + return 1 + fi fi - if [[ "$key" == "ENTER" ]]; then - echo "yes" - echo "" - return 0 - else - echo "skip" - echo "" - return 1 + # For other updates, just show instructions + # (Mole update check above handles the return 0 case, so we only get here if no Mole update) + if [[ "${BREW_OUTDATED_COUNT:-0}" -gt 0 ]]; then + echo -e "${YELLOW}Tip:${NC} Run ${GREEN}brew upgrade${NC} to update Homebrew packages" fi + if [[ "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then + echo -e "${YELLOW}Tip:${NC} Open ${BLUE}App Store${NC} to update apps" + fi + if [[ "${MACOS_UPDATE_AVAILABLE:-}" == "true" ]]; then + echo -e "${YELLOW}Tip:${NC} Open ${BLUE}System Settings${NC} to update macOS" + fi + echo "" + return 1 } # Perform all pending updates # Returns: 0 if all succeeded, 1 if some failed perform_updates() { + # Only handle Mole updates here + # Other updates are now informational-only in ask_for_updates + local updated_count=0 - local total_count=0 - local brew_formula="${BREW_FORMULA_OUTDATED_COUNT:-0}" - local brew_cask="${BREW_CASK_OUTDATED_COUNT:-0}" - - # Get update labels - local -a appstore_labels=() - if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then - while IFS= read -r label; do - [[ -n "$label" ]] && appstore_labels+=("$label") - done < <(get_appstore_update_labels || true) - fi - - local -a macos_labels=() - if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then - while IFS= read -r label; do - [[ -n "$label" ]] && macos_labels+=("$label") - done < <(get_macos_update_labels || true) - fi - - # Check fallback needed - local appstore_needs_fallback=false - local macos_needs_fallback=false - if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 && ${#appstore_labels[@]} -eq 0 ]]; then - appstore_needs_fallback=true - fi - if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" && ${#macos_labels[@]} -eq 0 ]]; then - macos_needs_fallback=true - fi - - # Count total updates - ((brew_formula > 0)) && ((total_count++)) - ((brew_cask > 0)) && ((total_count++)) - [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]] && ((total_count++)) - [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]] && ((total_count++)) - [[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]] && ((total_count++)) - - # Update Homebrew formulae - if ((brew_formula > 0)); then - if ! brew_has_outdated "formula"; then - echo -e "${GRAY}-${NC} Homebrew formulae already up to date" - ((total_count--)) - echo "" - else - echo -e "${BLUE}Updating Homebrew formulae...${NC}" - local spinner_started=false - if [[ -t 1 ]]; then - start_inline_spinner "Running brew upgrade" - spinner_started=true - fi - - local brew_output="" - local brew_status=0 - if ! brew_output=$(brew upgrade --formula 2>&1); then - brew_status=$? - fi - - if [[ "$spinner_started" == "true" ]]; then - stop_inline_spinner - fi - - local filtered_output - filtered_output=$(echo "$brew_output" | grep -Ev "^(==>|Warning:)" || true) - [[ -n "$filtered_output" ]] && echo "$filtered_output" - - if [[ ${brew_status:-0} -eq 0 ]]; then - echo -e "${GREEN}✓${NC} Homebrew formulae updated" - reset_brew_cache - ((updated_count++)) - else - echo -e "${RED}✗${NC} Homebrew formula update failed" - fi - echo "" - fi - fi - - # Update Homebrew casks - if ((brew_cask > 0)); then - if ! brew_has_outdated "cask"; then - echo -e "${GRAY}-${NC} Homebrew casks already up to date" - ((total_count--)) - echo "" - else - echo -e "${BLUE}Updating Homebrew casks...${NC}" - local spinner_started=false - if [[ -t 1 ]]; then - start_inline_spinner "Running brew upgrade --cask" - spinner_started=true - fi - - local brew_output="" - local brew_status=0 - if ! brew_output=$(brew upgrade --cask 2>&1); then - brew_status=$? - fi - - if [[ "$spinner_started" == "true" ]]; then - stop_inline_spinner - fi - - local filtered_output - filtered_output=$(echo "$brew_output" | grep -Ev "^(==>|Warning:)" || true) - [[ -n "$filtered_output" ]] && echo "$filtered_output" - - if [[ ${brew_status:-0} -eq 0 ]]; then - echo -e "${GREEN}✓${NC} Homebrew casks updated" - reset_brew_cache - ((updated_count++)) - else - echo -e "${RED}✗${NC} Homebrew cask update failed" - fi - echo "" - fi - fi - - # Update App Store apps - local macos_handled_via_appstore=false - if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then - # Check sudo access - if ! has_sudo_session; then - if ! ensure_sudo_session "Software updates require admin access"; then - echo -e "${YELLOW}☻${NC} App Store updates available — update via System Settings" - echo "" - ((total_count--)) - if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then - ((total_count--)) - fi - else - _perform_appstore_update - fi - else - _perform_appstore_update - fi - fi - - # Update macOS - if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" && "$macos_handled_via_appstore" != "true" ]]; then - if ! has_sudo_session; then - echo -e "${YELLOW}☻${NC} macOS updates available — update via System Settings" - echo "" - ((total_count--)) - else - _perform_macos_update - fi - fi # Update Mole if [[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]]; then @@ -253,10 +130,17 @@ perform_updates() { [[ ! -f "$mole_bin" ]] && mole_bin=$(command -v mole 2> /dev/null || echo "") if [[ -x "$mole_bin" ]]; then + # We use exec here or just run it? + # If we run 'mole update', it replaces the script. + # Since this function is part of a sourced script, replacing the file on disk is risky while running. + # However, 'mole update' script usually handles this by downloading to a temp file and moving it. + # But the shell might not like the file changing under it. + # The original code ran it this way, so we assume it's safe enough or handled by mole update implementation. + if "$mole_bin" update 2>&1 | grep -qE "(Updated|latest version)"; then echo -e "${GREEN}✓${NC} Mole updated" reset_mole_cache - ((updated_count++)) + updated_count=1 else echo -e "${RED}✗${NC} Mole update failed" fi @@ -266,98 +150,11 @@ perform_updates() { echo "" fi - # Summary - if [[ $total_count -eq 0 ]]; then - echo -e "${GRAY}No updates to perform${NC}" - return 0 - elif [[ $updated_count -eq $total_count ]]; then - echo -e "${GREEN}All updates completed (${updated_count}/${total_count})${NC}" - return 0 - elif [[ $updated_count -gt 0 ]]; then - echo -e "${YELLOW}Partial updates completed (${updated_count}/${total_count})${NC}" + if [[ $updated_count -gt 0 ]]; then return 0 else - echo -e "${RED}No updates were completed${NC}" - return 0 + return 1 fi } -# Internal: Perform App Store update -_perform_appstore_update() { - echo -e "${BLUE}Updating App Store apps...${NC}" - local appstore_log - appstore_log=$(mktemp "${TMPDIR:-/tmp}/mole-appstore.XXXXXX" 2> /dev/null || echo "/tmp/mole-appstore.log") - if [[ "$appstore_needs_fallback" == "true" ]]; then - # Ensure sudo session is active and valid before starting long-running operation - ensure_sudo_session "App Store updates require admin access" || return 1 - - echo -e " ${GRAY}Installing all available updates${NC}" - echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}" - if sudo softwareupdate -i -a 2>&1 | tee "$appstore_log" | grep -v "^$"; then - echo -e "${GREEN}✓${NC} Software updates completed" - ((updated_count++)) - if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then - macos_handled_via_appstore=true - ((updated_count++)) - fi - else - echo -e "${RED}✗${NC} Software update failed" - fi - else - # Ensure sudo session is active and valid before starting long-running operation - ensure_sudo_session "App Store updates require admin access" || return 1 - - echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}" - if sudo softwareupdate -i "${appstore_labels[@]}" 2>&1 | tee "$appstore_log" | grep -v "^$"; then - echo -e "${GREEN}✓${NC} App Store apps updated" - ((updated_count++)) - else - echo -e "${RED}✗${NC} App Store update failed" - fi - fi - rm -f "$appstore_log" 2> /dev/null || true - reset_softwareupdate_cache - echo "" -} - -# Internal: Perform macOS update -_perform_macos_update() { - echo -e "${BLUE}Updating macOS...${NC}" - echo -e "${YELLOW}Note:${NC} System update may require restart" - - local macos_log - macos_log=$(mktemp "${TMPDIR:-/tmp}/mole-macos.XXXXXX" 2> /dev/null || echo "/tmp/mole-macos.log") - - if [[ "$macos_needs_fallback" == "true" ]]; then - # Ensure sudo session is active and valid before starting long-running operation - ensure_sudo_session "macOS update requires admin access" || return 1 - - echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}" - if sudo softwareupdate -i -r 2>&1 | tee "$macos_log" | grep -v "^$"; then - echo -e "${GREEN}✓${NC} macOS updated" - ((updated_count++)) - else - echo -e "${RED}✗${NC} macOS update failed" - fi - else - # Ensure sudo session is active and valid before starting long-running operation - ensure_sudo_session "macOS update requires admin access" || return 1 - - echo -e " ${GRAY}Contacting Apple servers (this may take a few minutes)...${NC}" - if sudo softwareupdate -i "${macos_labels[@]}" 2>&1 | tee "$macos_log" | grep -v "^$"; then - echo -e "${GREEN}✓${NC} macOS updated" - ((updated_count++)) - else - echo -e "${RED}✗${NC} macOS update failed" - fi - fi - - if grep -qi "restart" "$macos_log" 2> /dev/null; then - echo -e "${YELLOW}${ICON_WARNING}${NC} Restart required to complete update" - fi - - rm -f "$macos_log" 2> /dev/null || true - reset_softwareupdate_cache - echo "" -} diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh index 3ce00ec..ffc6531 100755 --- a/lib/uninstall/batch.sh +++ b/lib/uninstall/batch.sh @@ -406,7 +406,12 @@ batch_uninstall_applications() { summary_details+=("No applications were uninstalled.") fi - print_summary_block "$summary_status" "Uninstall complete" "${summary_details[@]}" + local title="Uninstall complete" + if [[ "$summary_status" == "warn" ]]; then + title="Uninstall incomplete" + fi + + print_summary_block "$title" "${summary_details[@]}" printf '\n' # Clean up Dock entries for uninstalled apps diff --git a/mole b/mole index d19ee0c..1310ea5 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" # Version info -VERSION="1.13.4" +VERSION="1.13.5" MOLE_TAGLINE="Deep clean and optimize your Mac." # Check if Touch ID is already configured @@ -181,7 +181,44 @@ EOF } show_version() { - printf '\nMole version %s\n\n' "$VERSION" + local os_ver + if command -v sw_vers > /dev/null; then + os_ver=$(sw_vers -productVersion) + else + os_ver="Unknown" + fi + + local arch + arch=$(uname -m) + + local kernel + kernel=$(uname -r) + + local sip_status + if command -v csrutil > /dev/null; then + sip_status=$(csrutil status 2>/dev/null | grep -o "enabled\|disabled" || echo "Unknown") + # Capitalize first letter + sip_status="$(tr '[:lower:]' '[:upper:]' <<< ${sip_status:0:1})${sip_status:1}" + else + sip_status="Unknown" + fi + + local disk_free + disk_free=$(df -h / 2>/dev/null | awk 'NR==2 {print $4}' || echo "Unknown") + + local install_method="Manual" + if is_homebrew_install; then + install_method="Homebrew" + fi + + printf '\nMole version %s\n' "$VERSION" + printf 'macOS: %s\n' "$os_ver" + printf 'Architecture: %s\n' "$arch" + printf 'Kernel: %s\n' "$kernel" + printf 'SIP: %s\n' "$sip_status" + printf 'Disk Free: %s\n' "$disk_free" + printf 'Install: %s\n' "$install_method" + printf 'Shell: %s\n\n' "${SHELL:-Unknown}" } show_help() { From 694f5e76dbd1a9d287d8a1f2cb1b7580bebe8f7d Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 03:57:08 +0000 Subject: [PATCH 17/18] chore: auto format code --- lib/check/all.sh | 4 ++-- lib/manage/update.sh | 2 -- mole | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/check/all.sh b/lib/check/all.sh index 6a453aa..c419a68 100644 --- a/lib/check/all.sh +++ b/lib/check/all.sh @@ -280,9 +280,9 @@ get_software_updates() { pending_updates=$(defaults read /Library/Preferences/com.apple.SoftwareUpdate LastRecommendedUpdatesAvailable 2> /dev/null || echo "0") if [[ "$pending_updates" -gt 0 ]]; then - echo "Updates Available" + echo "Updates Available" else - echo "" + echo "" fi } diff --git a/lib/manage/update.sh b/lib/manage/update.sh index fd00fb6..8337cfd 100644 --- a/lib/manage/update.sh +++ b/lib/manage/update.sh @@ -156,5 +156,3 @@ perform_updates() { return 1 fi } - - diff --git a/mole b/mole index 1310ea5..09c6ff7 100755 --- a/mole +++ b/mole @@ -196,7 +196,7 @@ show_version() { local sip_status if command -v csrutil > /dev/null; then - sip_status=$(csrutil status 2>/dev/null | grep -o "enabled\|disabled" || echo "Unknown") + sip_status=$(csrutil status 2> /dev/null | grep -o "enabled\|disabled" || echo "Unknown") # Capitalize first letter sip_status="$(tr '[:lower:]' '[:upper:]' <<< ${sip_status:0:1})${sip_status:1}" else @@ -204,7 +204,7 @@ show_version() { fi local disk_free - disk_free=$(df -h / 2>/dev/null | awk 'NR==2 {print $4}' || echo "Unknown") + disk_free=$(df -h / 2> /dev/null | awk 'NR==2 {print $4}' || echo "Unknown") local install_method="Manual" if is_homebrew_install; then From 825992cdbb237da2df74bd8405b104b745d9fd98 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Wed, 17 Dec 2025 03:57:46 +0000 Subject: [PATCH 18/18] chore: update binaries for V1.13.5 --- bin/analyze-go | Bin 7298178 -> 7298178 bytes bin/status-go | Bin 7422050 -> 7422050 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/analyze-go b/bin/analyze-go index c257f823b3cff1f3e8919064e843e8f2c47d154e..7f665e2ff114f59809a6f7f2cd7d36b82bb10cec 100755 GIT binary patch delta 1211 zcmb`_No*4b6b4`w3QkhmI?!x234__3G|qU&@r)gyi5C(lu^odGd+ZQ`XFTIA#>vPS zLo3CAkbOXaK*%7m2MG=>YAy_@DnbqF6;Wpuho370Lyn_V29+PKd*Id55Q8 z|9eO7{G!UB`J{i%n_23P>NKGa3>z3Ol5$Hzg3HQ`UT_@> zN+hQr&zf`5ct)Q|_3ih2BKpAqX|!iJjv4Ch6*-KXs)cRH8DnuKFly|mwtjupoI;v5jMeQ*aA9eg*NyK+CdK; zN~MFmF#Ew{mJqY4G+C6;-69DFpnwJ=bb<*OCBMivj;rVD^4-6tEDy?m6=&2}zey@L z`^>Q_Up&MZ5*d5KXNw93S6WW-a-6itBLUBVJDPK*L`gPE6NelYG9^yXVL=}D4DAS6 z9Xm|}Nh?Lt2a;BIA|1%-NBC^;pb+zR4i*Hj87$BRTVWe)hp(X2lY90VZ*qp%ysU=NJL zUI+sZ0z^PmoUCN;|Bx{Y_)&;L4C0W0Buqd`u@?6|k$Q}?=bJACe_^jL{WjA-<2$|H zY2N&{ab5YuYB*^IL!Psk+MtYQ$w#^Utd_?894sG;3d;GfZzk8q@f$P2XI7 zERxYpj)qiIQ9W_(lA_&^&g+QbZ4X@?4!0)f(`oB(hZk<0s;oV+Fc=YI)!fNHWt;Nz IVEVrLU(gY?ZvX%Q delta 1211 zcmb`_TWm`K7zgmUZpNs&ZOWW(E_2z=r9J1g7^|(_Sht>TYu6p7J3Xhzw7Sj8hKL+Q zx*+13N+w1KBP6alE^(WPWS-`dOeBO%2#I+hK?r|u9z2PM-^=&#<)8oeZMk}5&=xpn zO*goDPG2+?vCCe3$?bd1bfulAuO#gy4AoAruh`S#3{kXD&pLRO_gQRYd6g^V5;la_ zd!w8;5|Nu**7BseaE;&3xt-3CKNyVI1&hjrNx!vPaYdUfN-W4Zqf#i~ZkDrBh@bm5 z6B%1IBCZhwU?2>F!7v1dLJ}myFi3&nFalCxB#Z(BjD|EA17l$vjE4!34jC{}*9ha0 zp`$69W^);yp;(z?&5DKA=^4i5NjZOxEK*RisS+0qnJKGE@ga+Bkrjz$Z7P$iHMWf{rSlEdxs#2n%}g^QKcY*461G8$UNNOE&kx!WPyi#+U-h@B?O z0*y**xmX&oi4i|1)(Af9;T0q={O98{@e;lQDxnIz;Dc)LLk)i%FlRqY z@63JK746#hb5H9!zr4S7wL8@$?94a4Z%FHn8GG)f?J?{=8X54g@_F!XpP}a&ec!sT z)=-$TjnK2Q8?_vwW3%H%@$1?Xp6m+@<;Xj8ZPWEzm-gS`PF?PJLan*^Nbf3bylwae Dg0#K^ diff --git a/bin/status-go b/bin/status-go index 5a084efb33ef67b6e75ee48dcbf707e4b73ba4a1..38e7510210459a90e3336c7fdf9113fab94d7840 100755 GIT binary patch delta 1145 zcmb`_NlX&~6b4`rkWHY7vWVgUE(nyFX=geupja$z*~&777F0?%x=^4~6-Z-Z3C2ca zjHn3*F9t4%F;OuYZxRD_xrl~?1QHWs3^8!=DuTU)v1Op_0N@r_s>{ppv1Db%RlMxhDT|pA@^ZE#p zGa4eoAtD;KKn%n}9BhSnNPt90f@IhRDUb?jAc1t)4jHfmc0wj(K{n*TE-^&po{LUr z6>^o-s!(uN+RAYpZ5Jg4xpVPFe~+9hrOZk9$Yc!1+LW}NlTi*@W|!HlPV0ErFrOF4 zD$3=m@sVMk3>%7bVK?l7JlG2a9TP0wh@Vk=i(K3*st%)EL@EJ{?ZMe|;DgW2NK`2F?n zK(*Oa=W$quNSnnZZ)xjmX80OoU#(78qjW3CHb=8(DCnZ74z5-wS9kaf8Z+7C61+yL zQQdB$JBNI{jdxJT!uDhpltTqn!eKZ9Rd5v4Pz}F#HBbXu(19Lmp$_Vy0UE&oP0$Q2 z&??%K`EQPY+|%#pBmY%>7}bLj+MpdwV1^FpgyV1mPC^%0pc|}U13Ngt2|eHvM~2;r z&o(pVp9fvmd&0|lFZ6;B`k)^M#AUtzX_yc)ZY@S6^LH259g|o59X}>h>c6i~zni0O z5Ck(2=n|hS_-7en_G8)DMpn=`)P7mj7O`C|V0H-&aVSza_RPQ(eJ5+b2A na`wSm`RS<7HwT~3f4TjzVj?6Kluks9`a6-NjZv|h7KpT8l;f44 delta 1145 zcmb`_TS!v@90qW#%+%C*D=%G6re&J$Y&*woX4x)o({#=qb(@uXJZGEE&0Wn4L|X~N zE)N=H&_hueD+sN?(_3NpdNB&agrbPBhu$iP0{fts9(oENelP#0kMH|`2tQo?0j3o> zBE_Y>FTPDGr1z(Sb1CGz>o+BL62=LUrOz%8D*Jh}+r(NtnqXt7&f6hoRUu9`Xdki| zjQSSF-EGnyXcMa}I%ioc?WG%9`rW;Lwck4&5ZAP+9cp7EYYUik9RZhB$y!YXNyPHm z2$6dtOhm&(9IS%Xum<8`EhNA?NQ5LvhV_sFsgMRDNQVr_ge=I04X_b%AQ$q4Fp+;O zE}fJ}=rXBXO43{fE#oPPAS%dzkWl>hiDJ80rGu^Lvdc(?gOuATj^cQhrj-uqMEfAK zC7zU2O6ZBmAR~?%OY&h8Y=$kc6$sb{1+X0op$K-sPAG<5Py(f}8_J*@#DcMetU9vt zQ5RxlbRi}IDae2V8Y)08EW{Mmog%IIWZL;uIq|vZ_1Lwg;|oG@qH-yY=?@HR>Y)LQ&1$J7G5igNV_`nX`X*0T#omLONmrn o*?hL*{lUq%*N