From 3fb08fdc24800b584cba529e168a58326a9fc204 Mon Sep 17 00:00:00 2001 From: Jokin Date: Sat, 11 Jul 2026 21:55:42 +0200 Subject: [PATCH] Simplify tray: static template icon, click focuses the app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The setImage-swapped progress frames lost the template treatment on macOS and rendered black-on-dark once transfers started. Dial back per user decision: the tray is now just the glyph (template image set once at construction, never swapped) and clicking it activates the window — no menu, no progress effects. Removes everything that only served the fancy version: progress frames + generation, engine.aggregateProgress, the workspacesSync webview->bun channel (schema, send proxy, panels effect, handlers) and the activate-tab action. Co-Authored-By: Claude Fable 5 --- README.md | 6 +- assets/tray/progress/p0.png | Bin 449 -> 0 bytes assets/tray/progress/p10.png | Bin 447 -> 0 bytes assets/tray/progress/p100.png | Bin 468 -> 0 bytes assets/tray/progress/p15.png | Bin 473 -> 0 bytes assets/tray/progress/p20.png | Bin 479 -> 0 bytes assets/tray/progress/p25.png | Bin 486 -> 0 bytes assets/tray/progress/p30.png | Bin 477 -> 0 bytes assets/tray/progress/p35.png | Bin 487 -> 0 bytes assets/tray/progress/p40.png | Bin 477 -> 0 bytes assets/tray/progress/p45.png | Bin 485 -> 0 bytes assets/tray/progress/p5.png | Bin 457 -> 0 bytes assets/tray/progress/p50.png | Bin 476 -> 0 bytes assets/tray/progress/p55.png | Bin 482 -> 0 bytes assets/tray/progress/p60.png | Bin 479 -> 0 bytes assets/tray/progress/p65.png | Bin 485 -> 0 bytes assets/tray/progress/p70.png | Bin 492 -> 0 bytes assets/tray/progress/p75.png | Bin 485 -> 0 bytes assets/tray/progress/p80.png | Bin 483 -> 0 bytes assets/tray/progress/p85.png | Bin 482 -> 0 bytes assets/tray/progress/p90.png | Bin 477 -> 0 bytes assets/tray/progress/p95.png | Bin 481 -> 0 bytes scripts/gen-icons.ts | 23 +------ src/bun/index.ts | 10 +-- src/bun/rpc-handlers.ts | 6 +- src/bun/transfers/engine.ts | 29 -------- src/bun/tray.ts | 122 ++-------------------------------- src/shared/rpc.ts | 10 +-- ui/src/app/app.ts | 7 +- ui/src/app/panels.service.ts | 17 +---- ui/src/app/rpc.service.ts | 16 ----- 31 files changed, 17 insertions(+), 229 deletions(-) delete mode 100644 assets/tray/progress/p0.png delete mode 100644 assets/tray/progress/p10.png delete mode 100644 assets/tray/progress/p100.png delete mode 100644 assets/tray/progress/p15.png delete mode 100644 assets/tray/progress/p20.png delete mode 100644 assets/tray/progress/p25.png delete mode 100644 assets/tray/progress/p30.png delete mode 100644 assets/tray/progress/p35.png delete mode 100644 assets/tray/progress/p40.png delete mode 100644 assets/tray/progress/p45.png delete mode 100644 assets/tray/progress/p5.png delete mode 100644 assets/tray/progress/p50.png delete mode 100644 assets/tray/progress/p55.png delete mode 100644 assets/tray/progress/p60.png delete mode 100644 assets/tray/progress/p65.png delete mode 100644 assets/tray/progress/p70.png delete mode 100644 assets/tray/progress/p75.png delete mode 100644 assets/tray/progress/p80.png delete mode 100644 assets/tray/progress/p85.png delete mode 100644 assets/tray/progress/p90.png delete mode 100644 assets/tray/progress/p95.png diff --git a/README.md b/README.md index 0ab8784..5eeb718 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,8 @@ remote server over SSH/SFTP, so you can move files **local↔remote**, (MB/s or Mbps), persisted across restarts. - On macOS the tab bar is fused with the window titlebar; other platforms keep a standard titlebar. -- **Menu-bar (tray) icon**: template icon that adapts to light/dark; during - transfers it fades and fills an aggregate progress bar (delivered/total bytes - of the whole queue). Its menu lists your tabs with live per-tab transfer - info — click one to jump to it — plus Show and Quit. +- **Menu-bar (tray) icon**: template icon that adapts to light/dark; click it + to bring the app to the front. - **Edit files** (F4 / toolbar / File menu): a built-in editor works on local and remote files directly; or pick an external app (Options → Editor → Custom → Choose…). External edits of remote files download to a temp folder, diff --git a/assets/tray/progress/p0.png b/assets/tray/progress/p0.png deleted file mode 100644 index 7b79961bd82fd1281177b936539ad36e4fe570b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 449 zcmV;y0Y3hTP)+uT=s}5>yCm&h zE+r4%kdWc!2=~ur8$DwP4$Tl8nzanxLl>WOqu>iZ$6fUCh!YHsD_ARghIM$yI}FaM zf*){-!D$oxf%o{0(PxL3b#=AI*V=^EIN&L1#4hv za2QOmuth)P3kJm%EDkV7g_QT%a&(kwm*FxfI>#Rj9xF2Rv58v65r$-j1^!|szTj;< zK}E+vu}}DgR(!#SxQ{WJ;wxHNyWrk^5<;@6#`R2&dZ7tg$uI^-c#H;H^d%Z@(0Z_q6|7HTag2*{^kT5ihdL+zBUZ5RMJE?5 zT+tV3NG4cRMeBFgkW{d+MK6+Euy92u5v+aI?8QEk2-a%qoLGk>g3H6BiUM!ZO3Yxf rGuMT6Weu@|mu3hK%@7=#-Ua^wXQ^K;-eHk`00000NkvXXu0mjf>rA^! diff --git a/assets/tray/progress/p10.png b/assets/tray/progress/p10.png deleted file mode 100644 index 18c4dfcfd3f564a9b5baf6d960860f74617fe627..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 447 zcmV;w0YLtVP)8N7}zKBh*&7rci<^l^=242~;UE4zcU@PZc@ zoJ9p+;S&a@N$?w9;ul7zNw8=wx=!!}*U->Pbe-T6)bCcK>jWR;0QFyM(RG7GtI@7t z4Gawqg9#S4=v%zUptyp?2Ii=c@;O_Mjxy~soCihc_>IA1MTR~`_=~R?l6Rco4_4v} z-ozUECw5Xua6R3f8Ak9OI%KT@2R!Q1`@t#0nO^=;VTh zEBXKp$pnk4=ph=C3Kq8LBFO~{S9B7=I%myU>?4U_t)}jYvyen^d3aPY!85cHGgz!n pbzxmuL+s$W>4QVl2ZyF}!GAgEYc0GiLht|p002ovPDHLkV1fy~#B=}v diff --git a/assets/tray/progress/p100.png b/assets/tray/progress/p100.png deleted file mode 100644 index b9fd386a8fad75c6204f24368f0e46147921e0bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 468 zcmV;_0W1EAP)pff+EoU$RSPIEaDma=p0+HPPT*dFu(_N z&a#4^@DrU=CHM;;@fW>QC0NuJT_$*hJ7}mSx=ipb>UFKrWrA;T1NB~Q(Pe{0t3cRE?Pc4=xCn|)@DH8GiWrM%;VB-VOEgkF9AYj- z!Tb1v+vtODJVK3NCPl&BD2gu8C)aot-8i`ljLLdC#Ayys<(ZVzI@zCAl z5ObL)cv&QpTS~tqK(XxHTVu*m?qdYO|WZLF8Ci9XJ0M787n3L0000< KMNUMnLSTYig2e^^ diff --git a/assets/tray/progress/p15.png b/assets/tray/progress/p15.png deleted file mode 100644 index 982ce199bf220d9d7f24839ce6896cb92fe12d91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 473 zcmV;~0Ve*5P)G-w%3F z;w9I#z00-agKual<;x|xKbIy8MicCtCfGMi8N7*Qe2ol(EBFvw=;9H(=$*1)t?U6# zLLcwYJM#*Dz#e*Mn&1z-#~%#NG{K^_=qkYj+(1Ju(N%&kQJ-s#t`dBKE2!^ki>?|h zYK<-n*1*ud+3W# z9N-WOaRu+=Ev}>f@Dw}fiy=PYH(GH8pC?Ijf`%JBL!am_cF@Yw1#hEn(HbtqBO%(j zi*MLNUuZ#Qe(B~(hS~;Saq5YfVhA*VwBv}PF|OZas0-?%l@vpucVQ0=w&-ItT%q+~ z8!A|j!uS{$`RMUroe%r?k5IwF6&+o$D2qNqLo~snD7uA)sDgzpdYtHjMOkzd!P;ld zUhE=@V6CRkiIWgTaDI3cF~DoI5;9nC$m_FOXn P00000NkvXXu0mjf;w{z0 diff --git a/assets/tray/progress/p20.png b/assets/tray/progress/p20.png deleted file mode 100644 index 2fb92870904be73cfa9ebbe7016d0a5907d16c81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 479 zcmV<50U-W~P)9dBF#`iFtIehtA0g*2?bV zGz{<#oinfC2kfJB$^?JlJ^r9~$^?tbqKgC%aRUvNL>CEeqdr#}T_pGlS5V(o7F{%0 zR2rQXtbw7yVKBkM7X6H`=#;Erv5he*BzevzqrFVK2xmdjG5(_SSP@|!TX={q?BEN! z;1h>9!c_8tckl_<(Qu3B=mN!d@f)q=1z$EAjWbMgj9L*F^zjX?EM2g+gf7t<8rpb) zE*N1KOK%CWg-6)JM-2L=nfDt<02V78Laoi0sg~Ru*i!JE?8tmpP(U_V38KB$5}&A!NL|jNpQg; zD>{f^owMdF&LfCmt!5LaA&B7Q@JM5bH)zFYuvi_Ng>_{OzJq6`4t7l)?3#rO{sZ07 VYb|%n+))4k002ovPDHLkV1m0)*KPm+ diff --git a/assets/tray/progress/p25.png b/assets/tray/progress/p25.png deleted file mode 100644 index 4b0e5f1b0183f7cf0a732117201245ec55cac7a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 486 zcmV@P)g43qZ{!DXbL!&aq>GD1aTElpi-UBrlnSB_z8~}; z#7ok&xl3B|;0-OMyry^ePr_iyXo6kS1iNM>gWH(J=TtA`1s~ux=F!IiIwvbwS9Tv~ zVTjl0oU(#%@eQ3bPw)rc;5T|_o?uZ~bdlf@ZlR%)=pw;A)ay#4iv(Zb8tT2uqKgKL zN~5!aH83I7J&hJj1flUU@+)D;KOKp}%MY z<4mxNwObORg-6&yp-p#(V=R*iF1+c=Btjj7uQ>Na4ABP~Kzk_C#<)Hcp?=T+t%T?U z?S&2+Y|$rZxJ2uNt*>A$h4>g3$>?~no)3rk4`0C|FS>TYA}jhB4YdgtY0y{*EoUAiHUIzs07*qoM6N<$f|T0WdH?_b diff --git a/assets/tray/progress/p30.png b/assets/tray/progress/p30.png deleted file mode 100644 index b4fbd46cf3fa3304de7e22cbae5b8f6adbb3d8da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 477 zcmV<30V4j1P)vm)D~SPc!V2hs3p2ga1Zsm*61?9=eUA;ueRv2!J^h^ zSFi?#28Y1}3tRL9KA~4!!D1WRsF3VEn~e@K?NVF>MYr)Ay~m0ai`c?L?4V(ceSE~K zc*ha`U@k?$J9vldXrqT`SQU!B$4|6U6x>ac!r3?NAn~7^J T_Db5b00000NkvXXu0mjf%O2CU diff --git a/assets/tray/progress/p35.png b/assets/tray/progress/p35.png deleted file mode 100644 index 1b6cb884b323e75c7d9142296ef92577b32e98bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 487 zcmVg43)_o|bVi;I&kE*8a65Tq^+(#29Lh&uRw(1Q{$ zNz?Q$SIL7nv=sW9-rYZmqa~vWc1;uPntvJG#ymbIdZ8$I4>z%h9`?~WdBM7}yEqF2 zyh7(JD)!( zRmM2PAIzjEcpL9<9kY1D6Vw-0MX~qziB^h&yHOM!V-cs=jTVFU$}?JtF^_JvWWibz z`is^ui=dW-XyE~NFpW0dEsn4%W30UC7D<9S248UQi5Q~~G=O$cWQ}osCPDq6K3a*< z2iglAG}xk#&~Sm)2U}mkS_;!+T%@C?gY|qkz(4p37Dds)1&h4sLo@^vEV80?J8K9k zSlFVc2`*UVMF$bAbJm>2MFbJ7tJ%g`2qHK=JhB+#C0g+rELJDxVZE{j-@%n>f?d-D dyJqQve*t%VZ!I($z>)v}002ovPDHLkV1mi$!r11gVQt7fZ1q>frl91EtMf zdc7u>$~|~PONZAq$-kE-OGX{+n>yGx|1!9Nc??Ge!4-Ufo9Lp0UGz>~uvT^-XQ78T z=$%Cc-{CuYXP)2>yv1(}&OE`QvgjhgecV7pCDBEKFHx^6jV=;=jw`75DvK@}EGmu8 z3)aBU;4qkAVT*pk7xYSAu-L{XDrEAWos14L?NTg*qMP`G-eX0IF1GLxTWDi|9el}Mhkr#c8hG>FCRc~8-D{BZHT$(!AH+8UYmM-`YISp$q TLN3n`00000NkvXXu0mjfu3^mR diff --git a/assets/tray/progress/p45.png b/assets/tray/progress/p45.png deleted file mode 100644 index 1cba41b4f511dc3a13913efcaec93a3f5aed99c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 485 zcmVCf@L}xjY_zel(#FPMuoQ$48>=*ikbr0k=LcB` zm%F`>WcM!N2Bt^|++^9^KNr;2j4IeQRj_L|GPsF0CNsT|6nu`# z7~ms1XH~(^_=V2N6a0-&_>11j6D$gg&JsMtJv0;&ohA4V^|{dKEWtOph5D|r=&Zq_ z&}dt*28ITQ!32w_=r{a8r`UqUKDJOH;d?e5?Pb~pxD1MJ;U7AW6#*vEz!Pkvjd$$e z2=g$;Ax<%tq~HU5#a&E84=+&<&%+eE_=Axo1z*={wF}I`72aZAbXIy8$;Jh1OLTDu z)1U_xx+O#dPqB?Qbm(4ign1a_(uZ!9v{2XJC+41r0s24#=nzHR7}sxFs6W)lNCNbM z&O#dvQPJmUxW@@D;44^LVSbE@aP)kz-VX=3fUjVY6kWPtVT(RPLurCVTyz}`r3w~N z(esopSlFUV5v+UG+{H42D3sM)SE@o&!)S>SOJt*nr z(xko1rR2dIS_*mnliZ&qJ1wIN_DvV;o2?AqM;Ft%K`0A8!vU7@f+O^fD_D~~#Z?&N zBYJ03!O!@G-l-G(jZgTC!Ko808jG$HoM9ggjYL-ozD0d*G`dRg4ep}8Yb?5IuxK>e z6|9A!#bGeP!WR98@8}g*usFaJ6_P(^^U*=JU4rYN=oJ6Zd#p&%g{C*e414I43Fgyu?uGS@}RKTNm8ATcVGeaE3kf$pm#BMHf;Qyh&2j zG5CqKH)4Vz&;mL}QMAVOn-ukjMrb9$5FFwW8f?)QXt={EE)gnNx5DBY7y0PLV7(uX zafwjDqAWVPVBv~BM?*Bhq9}TRhNyytEqamYf`uzOieR0y<}5BFieODs?}@7rMR0z3 z6p`UQS_v5}_U5{Y!Gb0ySR=vde}$rlm%;LcW@F0 zc!l1XRqzeIqIaqUzvDH2V{obji`t@Hf`_<=H-uE7_qJP~6Ifd>)iz zYE9LJNzMFk6`dtBUId;k}9b#n3=hkFTj-C<@-g4i@o<19VPauvT^# z%P_=Sbk4kj@9_hjQziHl@9+n`Qzcl`7Htze!VQe6CE6zV0`g*Xd}j_?-lhq|L_$oilQ4AEb^icF{UxWA}hLsF^viqrsyP% z3l@3NjR@8~8{fr6G$L55*~2n4A~-!fvKZkN8u1w{R;T7+T-g}k!86kayQU3x&B6u$ Y0rOmKEe7@O3;+NC07*qoM6N<$f;oN7Y5)KL diff --git a/assets/tray/progress/p60.png b/assets/tray/progress/p60.png deleted file mode 100644 index c66d77b4ac14b235d3072730b0f14deed6a968a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 479 zcmV<50U-W~P)uIYkZvy#C(n8nvjFXROu;Wp+mz#%#(D_Cdt5Q{Lz zK00Sn!4Eh>=Tr&)#C!Zf?^FpEjYXFUp5PW58i_6we1m%3Xmpw2Yg|LU*I0DfV9{uF zR&a39Awhp%9f7u~vGkrjQ4hSmg&v}paEHMA;N*rMaKE?8tm zw<1{ktl5k6XhpEjrk)dv(2C&X@JM5VcWA|Duvnj&h4suDdo^XIR zrWoN2GqDBl<1=n!6-L-c8#5f@HyW`8Uv)a23tR@5_=+~PS4L=L?Si!=^oq8y3JU0> zjTsKH^pX%gJjE)TPTH7ago@5&k>F(!qK?6LEIkne^nnJ@A&RUquHS^HKQu-o0s26D zA-SYX(dQU*i^hwsuV5{OSEdbiO&jc* bl?(m@IfiR38?*AC00000NkvXXu0mjf9p2I> diff --git a/assets/tray/progress/p70.png b/assets/tray/progress/p70.png deleted file mode 100644 index a5767d39559a5380bf335c372114d19a8f826ecf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 492 zcmVNUs*8)oBCdke#i@%KDu{OQ{Gfpl z?@gMIdy|$NIE0oCr@1%xPlDc((FMDv3wF&)2Jc`NUvs^X7kq@??rx??kU}1~i z#+X(G3sZEG)&+~K=vD;loQ==oJX#T~yQ%lYMQBBEdU)7~@D7dm3>NEivoPM-7~jE_ i>4II;1-oYHg8u+$u5K;q+n8to0000BsjzkjHo5LNboi4cdgMyg0FA|^L`?-Z-%3}4a25Z}?r(gkZv=n<`9RXpN3 znwVf8OK%C$!6U5drIRMc7^0$6nI*U?0@N}1fu$#+k1o&v+Cz~x#&u1Ax}gCY@zDi( z7vfvW6n%mbH)#B@brr0w5Fg_r866Mq;t2oYDp=%2H!fIYMIU2CV}eCm^d?3$Dp;7J z<1{WN@s b3m5zc*8giQ(lABv00000NkvXXu0mjffb-82 diff --git a/assets/tray/progress/p80.png b/assets/tray/progress/p80.png deleted file mode 100644 index f53be3687f4cfbfb35f5a5eaa84b45aad2e62bd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 483 zcmV<90UZ8`P)+uT=s}2= zq-k@PYsrI0Y3cBKx!k{rqdB7sc1;)TnuQGB#4fR3I%k^T54^!|^v*QFqP6Hc!6~kxp_S-5!I!A_twz@gzQ85aXRSro4Hm6N zmj!EJXmA)zu&_lx;R`yYELdz|f(pr>v-xN*(=NtYP;`Pn=sZ@$Sj7e&;0`u{u2diO znq`D3{$e3j!Q1$NtJoxpJHuzpqS#0LLMv6l=TQ`$pbPHs6!U21U(w3K1#3&_60Km9 zINK6p0}rvuo9-U2RRj^N)zow1Bm@zhA09=dc#T$k28-pfWms3%;5&F`x?tCI!LFIR Z;6DReZ!IBh9+Ln7002ovPDHLkV1f!b*DU}5 diff --git a/assets/tray/progress/p85.png b/assets/tray/progress/p85.png deleted file mode 100644 index 26b85d86a2e973ea7758d842de3b7c99ca704257..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 482 zcmV<80UiE{P)Yb6bi+6fp39BVK9IIgJEI8laOIhGfa$xzy$A??2=Nh z?b`LO-O`sl(PiSTySslI26ILm?3y;%H47QMfmwV`^g>?n9-J<2F{wBX-d+ z!Xb_^mAv3Be8N?9!400FL9spjLMwT}=Rpu0qf1Wk6%E=e@#{YbWLLGx|Sb8Fc=mQO)9TaI}T;GXMKd6sZLiB<5LVQWt zqL0vUh1M5aU%^@m@i8uv(eYrt9}e&zzJf(wbmM|WR`ekn8WSwiqV+gyXjHJUMaOAe zu*iyTM6k|Ta~9{(h+y4Ky(dmXBZ8B|BaIf`&;S4c diff --git a/assets/tray/progress/p90.png b/assets/tray/progress/p90.png deleted file mode 100644 index b5f51dea810094ec24e42288d4866a638fcb5bf5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 477 zcmV<30V4j1P)fv5{6Oc-6a0yfI708t6D(?rE)zV!Ei}{;T_*S%^}W{UGQn54hWf3x=(540 z*66%o4Gawqg9#S4=vRD0r{o2TT})6R>3udG?Pc1A`3e#g;q@$;Ubw2FlKYRs?qUhj)MPBp?8iENHS<%~Q2r5|EqNfQiSmZ?q z5v+aI?8QX{5v2wx1)*pM-w%3F z;w9I#y-Qm1;0-MuUN6c0b5Uc#sDph|2m59zgSRn{?~y@p1s~uhx@ch+y^|NLmEFfl z=;1wjXI{aN_=(<`Cin{<@CSo4O|U2}x=3(>8)zscx=8Rf>T{{lMS`zz1@&EN(M5wr zsnL1C8W@w(a26CD<1c!T6%o4F!b5DL5Bk`` zHYPa4OkBac_>Al5n>#$m4i3V>z2?ZT0`Gl;{|qbfL2_= zLJ!>$Hqi%te8M&+IK)gQ37!`*Y8(8(sV8EDAt3j1?yHAALAk&Jszy{VGsWiDpt=CyYrGkYmdYsAy zi@fMc1Z$r)d$Eg31Zy>QPMm~F1gD2b775;>m5{+=bz~mal{JJ8o|!t>H+8UY7B2V? XD|crtYK4;O00000NkvXXu0mjf)#S>} diff --git a/scripts/gen-icons.ts b/scripts/gen-icons.ts index cb73429..273bb21 100644 --- a/scripts/gen-icons.ts +++ b/scripts/gen-icons.ts @@ -1,7 +1,7 @@ // Regenerate every raster icon asset from the SVG sources (dev-time only). // bun scripts/gen-icons.ts // Produces: assets/icon.png (1024), assets/icon.iconset/* (macOS app icon), -// assets/tray/tray-44-template.png, assets/tray/progress/p{0..100}.png. +// assets/tray/tray-44-template.png. import { mkdir } from "node:fs/promises"; import path from "node:path"; import { Resvg } from "@resvg/resvg-js"; @@ -31,23 +31,4 @@ for (const size of [16, 32, 128, 256, 512]) { const trayGlyph = await Bun.file(path.join(assets, "tray", "tray-glyph.svg")).text(); await writePng(path.join(assets, "tray", "tray-44-template.png"), trayGlyph, 44); -// ---- tray progress frames: faded arrows + centered bar filled to pct ---- -function progressSvg(pct: number): string { - const trackX = 4; - const trackW = 16; - const fillW = (trackW * pct) / 100; - return ` - - - - - - ${fillW > 0 ? `` : ""} -`; -} - -for (let pct = 0; pct <= 100; pct += 5) { - await writePng(path.join(assets, "tray", "progress", `p${pct}.png`), progressSvg(pct), 44); -} - -console.log("icons regenerated: app iconset, tray template, 21 progress frames"); +console.log("icons regenerated: app iconset + tray template"); diff --git a/src/bun/index.ts b/src/bun/index.ts index 0ff2ce0..997b45b 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -38,7 +38,6 @@ const edits = new EditManager(registry, engine, settings); const events = { sessionClosed: (_info: { sessionId: string; reason: string }) => {}, connectionsChanged: () => {}, - workspacesSync: (_sync: import("../shared/rpc").WorkspaceSyncInfo) => {}, }; const rpc = createAppRPC({ registry, engine, store, settings, edits, events }); const url = await getMainViewUrl(); @@ -65,16 +64,11 @@ const win = new BrowserWindow({ rpc, }); -const tray = new AppTray(win, engine); -tray.onAction = (action) => rpc.send.menuAction({ action }); +new AppTray(win); -engine.onUpdate = (snapshot) => { - rpc.send.transferUpdate(snapshot); - tray.updateProgress(); -}; +engine.onUpdate = (snapshot) => rpc.send.transferUpdate(snapshot); events.sessionClosed = (info) => rpc.send.sessionClosed(info); events.connectionsChanged = () => void applyApplicationMenu(store); -events.workspacesSync = (sync) => tray.updateTabs(sync); void settings.load().then((s) => engine.setConcurrency(effectiveConcurrency(s))); void applyApplicationMenu(store); diff --git a/src/bun/rpc-handlers.ts b/src/bun/rpc-handlers.ts index bf3262e..fcf367a 100644 --- a/src/bun/rpc-handlers.ts +++ b/src/bun/rpc-handlers.ts @@ -1,5 +1,5 @@ import { BrowserView, Utils } from "electrobun/bun"; -import type { AppRPC, WorkspaceSyncInfo } from "../shared/rpc"; +import type { AppRPC } from "../shared/rpc"; import { effectiveConcurrency, type Result } from "../shared/types"; import { readFileText, writeFileText, type EditManager } from "./edits"; import type { SessionRegistry } from "./fs/registry"; @@ -23,7 +23,6 @@ export interface AppDeps { events: { sessionClosed: (info: { sessionId: string; reason: string }) => void; connectionsChanged: () => void; - workspacesSync: (sync: WorkspaceSyncInfo) => void; }; } @@ -203,9 +202,6 @@ export function createAppRPC({ registry, engine, store, settings, edits, events return {}; }), }, - messages: { - workspacesSync: (sync) => events.workspacesSync(sync), - }, }, }); } diff --git a/src/bun/transfers/engine.ts b/src/bun/transfers/engine.ts index 6563a54..db9a0c0 100644 --- a/src/bun/transfers/engine.ts +++ b/src/bun/transfers/engine.ts @@ -146,35 +146,6 @@ export class TransferEngine { if (job && TERMINAL.includes(job.snapshot.state)) this.jobs.delete(jobId); } - /** Live totals over every non-terminal job (drives the tray progress icon). */ - aggregateProgress(): { - activeCount: number; - doneBytes: number; - totalBytes: number; - bySession: Record; - } { - const result = { - activeCount: 0, - doneBytes: 0, - totalBytes: 0, - bySession: {} as Record, - }; - for (const job of this.jobs.values()) { - const snap = job.snapshot; - if (TERMINAL.includes(snap.state)) continue; - result.activeCount++; - result.doneBytes += snap.doneBytes; - result.totalBytes += snap.totalBytes; - for (const sessionId of new Set([snap.srcSessionId, snap.dstSessionId])) { - const entry = (result.bySession[sessionId] ??= { count: 0, done: 0, total: 0 }); - entry.count++; - entry.done += snap.doneBytes; - entry.total += snap.totalBytes; - } - } - return result; - } - /** Fail every non-terminal job that touches a dead session. */ failSession(sessionId: string, reason: string): void { for (const job of this.jobs.values()) { diff --git a/src/bun/tray.ts b/src/bun/tray.ts index 4a97d45..167d2a0 100644 --- a/src/bun/tray.ts +++ b/src/bun/tray.ts @@ -1,130 +1,22 @@ import { Tray, type BrowserWindow } from "electrobun/bun"; -import { BRANDING } from "../shared/branding"; -import type { WorkspaceSyncInfo } from "../shared/rpc"; -import type { TransferEngine } from "./transfers/engine"; - -const BASE_IMAGE = "views://assets/tray/tray-44-template.png"; -const PROGRESS_IMAGE = (pct: number) => `views://assets/tray/progress/p${pct}.png`; -const REFRESH_THROTTLE_MS = 500; /** - * Menu-bar icon: the app glyph when idle; while transfers run the icon swaps - * to pre-rendered frames (faded arrows + fill bar = queue doneBytes/totalBytes). - * The menu lists the tabs (synced from the webview) with per-tab job info. + * Menu-bar icon: the app glyph as a macOS template image (adapts to + * light/dark). Clicking it brings the app window to the front. No menu is ever + * set, so clicks arrive directly; no setImage swaps, so the template + * treatment from construction always applies. */ export class AppTray { private tray: Tray; - private tabs: WorkspaceSyncInfo = { workspaces: [], activeId: 0 }; - private lastRefresh = 0; - private refreshTimer: ReturnType | null = null; - private currentImage = BASE_IMAGE; - /** Forwards tray menu actions the UI must handle (activate-tab:). */ - onAction: (action: string) => void = () => {}; - constructor( - private win: BrowserWindow, - private engine: TransferEngine, - ) { + constructor(win: BrowserWindow) { this.tray = new Tray({ title: "", - image: BASE_IMAGE, + image: "views://assets/tray/tray-44-template.png", template: true, width: 22, height: 22, }); - - this.tray.on("tray-clicked", (event) => { - const { action } = (event as { data: { action: string } }).data; - if (!action) return; // icon click with a menu set -> macOS opens the menu - if (action === "show-app") { - this.win.activate(); - } else if (action === "quit") { - process.exit(0); // electrobun's clean-quit override - } else if (action.startsWith("activate-tab:")) { - this.win.activate(); - this.onAction(action); - } - }); - - this.rebuildMenu(); - } - - updateTabs(sync: WorkspaceSyncInfo): void { - this.tabs = sync; - this.scheduleRefresh(); - } - - /** Called on every engine update; throttled internally. */ - updateProgress(): void { - this.scheduleRefresh(); - } - - private scheduleRefresh(): void { - const now = Date.now(); - const wait = Math.max(0, REFRESH_THROTTLE_MS - (now - this.lastRefresh)); - if (this.refreshTimer) return; - this.refreshTimer = setTimeout(() => { - this.refreshTimer = null; - this.lastRefresh = Date.now(); - this.refresh(); - }, wait); - } - - private refresh(): void { - const progress = this.engine.aggregateProgress(); - - // icon: nearest 5% frame while transferring, base glyph when idle - let image = BASE_IMAGE; - if (progress.activeCount > 0 && progress.totalBytes > 0) { - const pct = Math.round(((progress.doneBytes / progress.totalBytes) * 100) / 5) * 5; - image = PROGRESS_IMAGE(Math.max(0, Math.min(100, pct))); - } - if (image !== this.currentImage) { - this.currentImage = image; - this.tray.setImage(image); - } - - this.rebuildMenu(progress); - } - - private rebuildMenu(progress = this.engine.aggregateProgress()): void { - const items: Parameters[0] = []; - - for (const ws of this.tabs.workspaces) { - const jobs = ws.sessionIds - .map((id) => progress.bySession[id]) - .filter((v): v is NonNullable => !!v); - const count = jobs.reduce((n, j) => n + j.count, 0); - const total = jobs.reduce((n, j) => n + j.total, 0); - const done = jobs.reduce((n, j) => n + j.done, 0); - const suffix = count - ? ` — ${count} transfer${count === 1 ? "" : "s"} · ${total ? Math.round((done / total) * 100) : 0}%` - : ""; - items.push({ - type: "normal", - label: `${ws.name}${suffix}`, - action: `activate-tab:${ws.id}`, - checked: ws.id === this.tabs.activeId, - }); - } - - if (progress.activeCount > 0) { - items.push({ type: "divider" }); - const pct = progress.totalBytes - ? Math.round((progress.doneBytes / progress.totalBytes) * 100) - : 0; - items.push({ - type: "normal", - label: `${progress.activeCount} active transfer${progress.activeCount === 1 ? "" : "s"} · ${pct}%`, - action: "noop", - enabled: false, - }); - } - - items.push({ type: "divider" }); - items.push({ type: "normal", label: `Show ${BRANDING.appName}`, action: "show-app" }); - items.push({ type: "normal", label: "Quit", action: "quit" }); - - this.tray.setMenu(items); + this.tray.on("tray-clicked", () => win.activate()); } } diff --git a/src/shared/rpc.ts b/src/shared/rpc.ts index ce9de9b..3c2ff18 100644 --- a/src/shared/rpc.ts +++ b/src/shared/rpc.ts @@ -15,13 +15,7 @@ import type { // pulling in bun-only type modules. // // bun.requests = calls the webview makes, handled in src/bun/rpc-handlers.ts -// bun.messages = fire-and-forget pushes from the webview (tray state) // webview.messages = push events from bun, handled in ui rpc.service.ts - -export interface WorkspaceSyncInfo { - workspaces: { id: number; name: string; remote: boolean; sessionIds: string[] }[]; - activeId: number; -} export type AppRPC = { bun: { requests: { @@ -103,9 +97,7 @@ export type AppRPC = { }; clearEditLeftovers: { params: Record; response: Result }; }; - messages: { - workspacesSync: WorkspaceSyncInfo; - }; + messages: Record; }; webview: { requests: Record; diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index c63fb78..a45f3c1 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -67,13 +67,8 @@ export class App { }); } - /** Native application-menu / tray actions forwarded from the bun process. */ + /** Native application-menu actions forwarded from the bun process. */ private handleMenuAction(action: string): void { - if (action.startsWith('activate-tab:')) { - const id = Number(action.slice('activate-tab:'.length)); - if (Number.isFinite(id)) this.panels.setActiveWorkspace(id); - return; - } if (action.startsWith('connect:')) { const id = action.slice('connect:'.length); const connection = this.connections.connections().find((c) => c.id === id); diff --git a/ui/src/app/panels.service.ts b/ui/src/app/panels.service.ts index a864536..d5785f1 100644 --- a/ui/src/app/panels.service.ts +++ b/ui/src/app/panels.service.ts @@ -1,4 +1,4 @@ -import { Injectable, computed, effect, signal, type WritableSignal } from '@angular/core'; +import { Injectable, computed, signal, type WritableSignal } from '@angular/core'; import type { ConnectionConfig, FileEntry, SavedConnection } from '@shared/types'; import { RpcService } from './rpc.service'; import { ToastService } from './toast.service'; @@ -180,21 +180,6 @@ export class PanelsService { ) { this.newWorkspace(); - // keep the bun-side tray informed about tabs (names, sessions, active) - if (this.rpc.available) { - effect(() => { - this.rpc.send.workspacesSync({ - workspaces: this.workspaces().map((ws) => ({ - id: ws.id, - name: this.workspaceName(ws), - remote: this.workspaceIsRemote(ws), - sessionIds: [ws.left.sessionId(), ws.right.sessionId()], - })), - activeId: this.activeWorkspaceId(), - }); - }); - } - // a session died on its own — reset every panel bound to it, in any tab this.rpc.onSessionClosed((event) => { for (const workspace of this.workspaces()) { diff --git a/ui/src/app/rpc.service.ts b/ui/src/app/rpc.service.ts index cdd00cd..4fdd40d 100644 --- a/ui/src/app/rpc.service.ts +++ b/ui/src/app/rpc.service.ts @@ -87,20 +87,4 @@ export class RpcService { }); } - /** Fire-and-forget messages to the bun process (e.g. workspacesSync). */ - get send() { - const rpc = this.rpc; - if (!rpc) throw new Error('RPC is only available inside the desktop app'); - const ready = this.ready; - return new Proxy({} as (typeof rpc)['send'], { - get: - (_target, message: string) => - (payload?: unknown) => { - void ready.then(() => { - const messages = rpc.send as unknown as Record void>; - messages[message](payload); - }); - }, - }); - } }