From f7278a43ec1428fee04fd3dce3a922f634b1f93d Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 19 Jun 2026 12:43:00 +0200 Subject: [PATCH] fix(electron): crisp, flicker-free Linux tray icon (#4905) (#8484) Linux StatusNotifierItem hosts redraw the whole tray item on every Tray.setImage(), so cycling the 16 progress-animation frames made the icon flicker between a full and a partial ring. Show a single static running icon on Linux; Windows/macOS keep the animation. Also re-render the static stopped/running icons (light+dark, @2x) from vector sources at 24/48px so GNOME no longer upscales a 16px source. --- electron/assets/icons/indicator/running-d.png | Bin 253 -> 635 bytes .../assets/icons/indicator/running-d@2x.png | Bin 458 -> 1270 bytes electron/assets/icons/indicator/running-l.png | Bin 240 -> 614 bytes .../assets/icons/indicator/running-l@2x.png | Bin 441 -> 1222 bytes electron/assets/icons/indicator/stopped-d.png | Bin 307 -> 650 bytes .../assets/icons/indicator/stopped-d@2x.png | Bin 564 -> 1358 bytes electron/assets/icons/indicator/stopped-l.png | Bin 276 -> 627 bytes .../assets/icons/indicator/stopped-l@2x.png | Bin 530 -> 1318 bytes electron/indicator.test.cjs | 37 +++++++++++++++++- electron/indicator.ts | 7 +++- 10 files changed, 42 insertions(+), 2 deletions(-) diff --git a/electron/assets/icons/indicator/running-d.png b/electron/assets/icons/indicator/running-d.png index 8896288f2c69849c1c9ccaeecf537e29278fde30..6a9d2e5a31852766d31032f36b87b7b3f5982a60 100644 GIT binary patch literal 635 zcmV->0)+jEP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10tiV& zK~zYIrIyQT6j2a{zZs%}%uEoC4`6PBU}m8UArGKvmV&;3ub>O}>Rxkkp@L$vaOWFH z6vb>@7!bk4M{qK3X2j1$pUxbc)6+eqpg4=F`m5@6)qlF7GJ&+h*{>VWtgz1m_km9V z^dM*-_!w++V-eJpo>kY?q5sQ4&{&4*nmUuhN`ZIPpCNdlK2xu%t;naXUQ?e3AJu(z zsVYG?s_>mUU(#W$3+i@==@pa2jx$l^uhruan|ez!87r;;7a09j8~|U#C~g5i$^fbo6s1MB6Tj;Q`~!p~ VG=d!iEpGq-002ovPDHLkV1hO35byv1 literal 253 zcmV-RKIM0mN13dhH`JI9- zdj9njrY)9HI^g@ivhb@Pv(X)(SgNHP0d2r7z((K>U=6S$qU|_v92fyU2Mz;; zLZDV4g!+NUfvv!mIenZ0-U9Xlzvc2EDQj6QX^*6HDi70=UX;|El~2|gxF2{KSf0@C z0`Lpa@akQ_N?>7v-2^ZQ97xS5rP2;bZ^n$^l%#hgZI!f2Qa!{+-Mh9;(!1WMM`53& z_B7nmhRjGrM?Xp0E~z`^Km<#-q$ebuj_^IK6d{#{{Lv8SGm@T_ba@WqwzNyy;rX=q zCP_q6*o2Mz2}w7UK-`uaC4C>#->_oBDb8V7%%_^x0wpS3U&|_BO9c6j3dtwU{N}U* z7E78i#wS$m$cJ$lH5|snID{GRF@`g$CgzjMO=@1)yORX;tNH%?%tavYmz{>ow8t@0 zz%FBOO6hNvDSGh|E$~PIA77gK99J)vx=oComvmXi zSRGI@_DfotV;$5bT?qL3SJI;38dzc!jsyP`7_lg9#?8h(8SVbYaq>~a_-OAl?F3J02M%ySn2y_JMW5AKwno+ycC_Sx|88TJ#LenAXjN$ZL zu#GiXDyb1?(BksE;W8y@S)2gmvcy5m$R(XxO@6K1rZR{ZY`dsMGj0q= zb6_8T;Wls$-21D4$hRQQ2zM|z2)v(~QOX&xQ@>m8saO^oau2D@p;+Xgdl~Q;@BsN= ze#U^eaR=RbUUUixnDqgy!=?K+0M`LmM6^8vdkdg0004zNklHlMk+Yvstw~+*N;idY!A6+@I_hbdDR#IlgcHW5!A-0c!eOH| z{_k&d6I>TQ>7Ihu2p`Q#tEr`N7A5{$D?Bx_P`_6S9}9=KaZ{L8c--d+hcziYZ5{}t z|M|LG@A?u970&+~IN_~jTBmWp5Vi;#|N9+6xSUEHz1=bM`{O|&OAv&}H^vxY&-cd* zLjN>DzA*+1XTLwb5;~*_@{Q3+cv%cV2jNxyO<4M$2?K=l^>gAby>gtG2?vV_<0fIt z??k$glStnbPbHCy!jM{0`A@|Y20YNLz+%$G5n=8hjkICqY?g3C>jE2!Q9cS4`7Lc! zJ>XA;HH9_RCT964)M%3bdyNo|$TZFgT;_-nR!h5E#jnA#NoIf!Iv60c$weXDP*L}2 zo0}MI56soNzFl&NEe<;Gl~>L?Xp145m1UBD26DzHb!i!Rc>n+a07*qoM6N<$f>*uE A{{R30 diff --git a/electron/assets/icons/indicator/running-l.png b/electron/assets/icons/indicator/running-l.png index 145dfde88724021ac7192d04aabf3e28f8773164..7d1f6ed647053b370bda125c36edd173ffba3724 100644 GIT binary patch literal 614 zcmV-s0-61ZP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10rN>j zK~zYIwU*6JR6!7hza|0+fiXsg2jG9ju)u{04*-!ZiM)vs_xb?Jj|(-CpnKoIjDW@s zn@Nld8d)Xs=B)R21%4%A%t-NNv+aOl9B?<0ndQ1 zKnsvb3pg>|>_ChJ@D#8IbR4LFOB27>0d|3EDFR>_I5+1u@CH}|#+}UsunxR68v(z7 z+CTyWj}d+b<_4v2KJ!4`e470v-mvN-KLkdKz@6_X@WEv=nMbhaA}3*SWz`>qy5+yEX*dLpTvuvvAsU4l>c-Ap(Mcnv&|^t~X6h>>CD zT(tRrxM+(GXS#n>>{_IC{(e+^F^rdav#3fos6;lnMsErJ(R|N<`!@*8utu+k>%cjj6N=xCyU&~TT;_Jq^v?X=Pp?W6(m{7szSwTSXI?V2;J zOLU~B;L(FGKhBJ3k*Iny24LEabpV2@Z8?1%c^dQ`)rrgp6Dgr>FqV?2co>e4T q1V?I$GMS>9LqV4)wP8al0`LbT5jJCh?w zK~!jg)tXCeRz(!Ye^aWqKwAi}7+k4DB8EpuKpwIYV_3P+;!>jtY{0T}qtLn|gusTE zt|5e##D!5wj5P)mqec+11_Ys{Xh<~BQfQRpV&mqWybYDehz!=j4JP&LHUI3N=k4MCZfpb7Va0>WV ziUJUkTpc^YeZXZva`sC=FEBqhjvT;e0sDa41%T^53hV{CDkT8C4qQ(G=&l>xGp_&c z?lsdJ2D(cp0JH-iB{&`djskB0i-6XEjaK*Ho4_$Q>M?tVfwoczxRB|OIQj$F26W^I z#SUOQa3x~~c1>pC6i$Xcji1kv|VCEebr~ZyE4w zWG15Flw>dz^Qoq_0Et+*9?C=j*c<`hUKRKhGap=?Vxyp!%JWr@d<4UQ!Y~}d5K`V3 zNZ8V2J_U*8fyCaENB~JFFv6)BOf`x+DUfG!~AlYyY<} z(O#s!ofMH<a<=nLEdrj&H%(grd~A$4VT_rb zLGPxz?u7azekfb*Tu6_2XN5lh1h$IE$&7j$lbw-B65l28Tsvix*lk#Tk~|7LnQ?@Y z+CSJ)-z*|Ga_Bz=JmOssiO49_=fF9ov@VCEQ2PvcPDDN|U}L?yzK_)>(65AED&P=o z{jvktBqBovZN03nC$M^PwuZ*qdny!jT{ISRtLk=QCup!IXuja~j0CDEr*|k!7YLQp zPPkVIUm@)YSv3zd?ZBYIw6jh|Da^(02~mZ~?7#;K%Lp(pO28@YRZ3&PvIhWu1-7X4 zvFvV;3n+GB_XJ;Y0GKt2;AaEBDGWnEXHx>sK(|sBw{%U`!PeIS|E~t+6AT>*7h zsd(f=r*a3PItj+wu*Zqo*gzFI;bLBgjR0SgaJ19RXczW~TN@p7OKz$qHbuzv1rk<( z@5y_-u1JJtLa>KU?}U;orQIGH*Li>Bi@o4b;dD6SNU&S|ao{cN19tW8 kyEOv*B~$TZj|biU2M|KXduyfX9smFU07*qoM6N<$f=H+(1^@s6 literal 441 zcmV;q0Y?6bP)kdg0004iNkl$M$2Bp5&34birUWzVeTARAxA$%}JkyHzRivQl-6Je>MSt*>;UXgV4T9~FO#|)Dv z6NFnW5*L2hsGBtPKlTfIG$({`FIhOlJhucmBV-8yArj6^7mfqBg;8k%31`L$H-N7~ z@3eq~Gkt|Gr3mOPe9hm54J}+4E!@g4h(`_8l1w;LN*ebH`_&iH?G}YpZ+N1J?g-Ix)nQ6rurjPwLa3O=JEZ5u)VmU+TK!sggWh8 zzuqo{^D^yPNnGZ<5VmPANmcz~*()pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10vAa{ zK~zYIot8aMR8bIy-z8C+WhrEhq2x;v6QS@&Konzvi8Vg~OR&bo!bWiuqZkXNr>8(v zu&1FAG2sWWyCh~e@>rZXGF;Ezd)G-OyLUd`cg~!1=H3dRKBQ{D?Xn321^!7%tCIE& zv}M|sv}eAFTm&Gc*MSDm<+8L)y>x+BKs|+(0-ptrEx7}{1?GUNix~pufeniR_yNom z5zM*@cYw(rEA%=A>{?DUOX36TuJRAS)e_)Q2Z4`nB_k<TlV`f zF%Op|HSGVPq(w>HocS3sw)`~p7i}t0)c|30M{_C{;lf3nG2bE)MlOWw{=cK*vthi5 zb{|kD3@QkNYp_r7P0Kw3uB8Y>xHRz$?-FK95%@Il#d4a!ZSPMd%(#_wfp>(HQFAdh z;GX&7Tkj|f-ittlDY?$Y+bT-i)Jq3=>D%J90=ZumBNkY8)c^nh07*qoM6N<$f|RouP5=M^ literal 307 zcmV-30nGl1P)2jki{t;)+peQ&0@XWLlrlJm+s zy`rd7GDy@0+q)hU_pvOq+dFO*>b61N*#^)w|^qZ?Fd3LOjyf}ff144S4pcUuzIo{5fFGpVuy5CMh977fk843RRBA&zgmpeM7RI|002ovPDHLk FV1ikaidO&t diff --git a/electron/assets/icons/indicator/stopped-d@2x.png b/electron/assets/icons/indicator/stopped-d@2x.png index b4063b1d36986d6b6cddf015968337394fddcae2..6b5175aa8d5e8937be0311f68438aa1c0c9ec9cf 100644 GIT binary patch literal 1358 zcmV-U1+n^xP)qPDuJj(y*it zC4DNXst_nu5kkF^?v~Ui>C6gk9Fp|Bq+ODJs>lLRlskbPz$~?+8Q>A1qbQ!DGjOw{ zMGj-*a(dbVJd-kl)4AFu*w4A^Ll+HM72 ziAFt%o&=inV9Oga!wDVz2=oK(IS0b$c3>lLIKlQ1IYKHA_`Lz+bHEnhO1lw3?l}^LX8N1Kg_D(COnKsXdECCpON*{HY0_I>GFD4!mGrix)6M&SNxuZ- zk4bvRyjvIl@Eurdgmza5FpWx*o|SZodB0uKb18ZsnCE${-c*g7X-e7|s^Nzyr}uz{ z483!V&XKq+7HBsDzvVDml=}kU_X2G>bQ^)=p59+TE8ZIDHd8bq>CcoYZIU~93`km@ zBIi%})8^f{r0XP2=g|Kn!W2_UG3Lm_3(UxTl`$d@d$LDynPigeS-{W6{tR$g!5RG8 z*f`&xfugj=``s6BlvXHhOl3(Gu`@0OSGS z!NB{EsXVW;+vsTA<_qmMLbv5GT*6flvgjZti!8`@7<=!>ZT=!2HZgWhj<`h9U6Ni* zk@;29wW_r8=4icnp2zBWd4=(EhWyD~1Mpg4)1$ zrh%R`2ZXB<`!q*-s!%Upg-a@Z+HU1SiXFHThEF57QpZAp?*hIz7{+j?vrGqsD=&68_Pk`<^g6|?+mD`!5lV+?LS5CAvLVec6D$2tKU=JxNNS4tK zT#4J#9B#`kvc;x_%r?pwEWj63QC|0*2+ahzq-qm^#g)_UX3F>ZnD%6W&+9-OPH9Qo zEG|{P9hW&KT`{f1C1!)9UlQ&jXXaY;lJ7=Ik16>cIHnVl#^er4CCy**HD&0H%bm5~ z%#A4L4A`k3zw&;0U?m$!XKYP^6fd!+#0mbk7MpOp7 Qod5s;07*qoM6N<$f)L}4OaK4? literal 564 zcmV-40?Yl0P)kdg0005|Nkl`oLwG}b zYfx8zMV}y@y7CoapmTOzJ&fMO1B8_}RHD*N4Wj3>8(|lDHaJhE%L$rb6~Y6W9NTE# z`vuVlA$+Ba3-&;`LUsVXJ#R5bH4N6MB+2wLE+A}0iL~M&Q*=N@iLAgBdnk3u#yaOI zOaJeWuuXq*6``FKdIVv!OOIQJP6(IuC+`s|tkBMUKp2+UI#eON^Gu+~aD)$Z@>$1V z6$tOWN6_HO5F>MYHQsLapOT z>NSM!UMssw80*9j@RYoS%S0*L*-lvu&*3D(T&s|VUuTVMVTp}}7u6i5>gvtp_=lIM>L-ajx@Qxqbi!hCGE^MXSXOCcsi1=PM6BW(qS6gY zZf$e%x3;Iur8u;?cGu>5h4;MY3cFcDck)`Klm7s6Cs$0=3L1a_0000pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10su)w zK~zYIoz}ljR6!I5@ZXv!&4P)7F_!$VSPLHlf*}?@fv;c(SZiWoqd;N=W1&6;g@S-@ zVCA302LPAEu+g#LT$o+BcQ4*#iuukt-*;xt%$eIrDXIH48jU8Tl-jj}Qc9%*6IjL` z+DI|l*bUowB}WPI4XmM)ktY0#kz*aKVx*P;ZsU6dA7LG{Xl6cxn8QZ+Bz!}wQXt5S z3_oC^YWWc}iLHn^=oQHwN3zV{;z~dAEOr3zvXu;{EYd_3i4u$_VXI-l@%G!boZW3T&@r(ioBBxEC;w=a$;7){sq_i z9Dvv1yI6M7CeI1F6ve&OhkQEY89fmQW0B>S|8EuB!SVbA&m2Y^H>%{gaSdFM{C&iJ z#q~1+;n>Q~@D`?OkWb^2({?@6Wj`mNm93-`MVrS^<}-x5Ve4d>&z#{a$!ZuYPG771 zEpGQTjY&+ApIf^?IaB z+M-a&fqjQ=t$K{F-UDydWjzJ(%C=!|ocRwab>kx1r5u|F7G=A_>?w#qTIi`xLmKV@ zzm&jxUjck_j8e)Am=k+T4#)VdxOF`*^}@K&wu)lcQrywxIQQK&2sC<`q}>5}%-h!H=f a0k{LzYdC8CBYE%u00001=;$EP)exMhqC>#~v_Ym;5pY%pL+C)C0FCC* zsWxMb0bT=c7G1;E+Sg-DKM~(+T7#wfiTY(yGegeMc}|RBfscTe1iPz+%~ahUz*UAJ z;EyEI4C4D0@rQwq6t)#$DX{wsXb&B@Oc0MJ5UvJx0V61xL&5ja_=9E|*x1AboEG+% zh7MdWI*%lfJs)6y0XD~AuK<1){PV!AO|IZk!Ev3;Ld~?T!RK|rs4-@f1Q}zxfDeJ5 z;I;+&t+n4|9J{_ngwRC7Yzzpz#U+8>08_zb4~cs+;~t6)A-Rdqqx`N-kx?dr%YFX= zrT-m*r)U%6<2SJf$uxRZ0;vIBP(+>ro>lIDLB_g@#xy$lPhS8U6`+Sx5F6YHf)p#S zaq=8q!J~rjv<6<@PGhP|Rm=e` zz`Hcg{GY(~tTXSqJ83NDwrC+_aPb=gcQ!Fl3vf`_=Pt*ACj{pSv~t>m2L<>HDNo3} z{c7Apt_nO>fcQ3GN@&_shYqOaDE=l~Q5k9( zZonyqowy^6&~OasT0!yMz>h-1G_W?&0Z*_)$tYD;?qKV4W&Hb8d<@Ym3dxtix{_yJ zJC!>SX%j=%iaSn}WMgIQ2_N$^Y%K68F(pV6X(#T8TaqsNl$$!mCWOq8vSBUoEm@D( zg%jFJSX}kFOWRy2d3V#8=fj!L_ku%&Q(V!ufGd?B!Bvh?ub8gE6=nyCe@XaaT-Y@2 zKp^6N8q03&TB7!n@2LO0l)`SBh-_1c1Hsad;GT*}g3?~Iz;LEe%(3~_) zum|YLs-uX%8W>;;?zpcH*aU2hfKLJ6z>% literal 530 zcmV+t0`2{YP)kdg0005mNklABQDSSFU`u$RvdG)m%YWknM-q=ZQC|~i&xjKy*GU>>GV9`=Xq-S zOEnd#skf1OX|KJ{F1p}?Q}+0#gnE{EA_}exCQ4Ofyt_iUV!ev4nrLHy*|xbZgfoT% z8ail~oYZO|95+;gyjrHaEqv1oi7Mex{`M;2tA$b&t7EDaQY;c)>x#^3;ibXK=;Mnp zHvjM8YsuT&x-YCproOwvLS@u)K-ixTV2N;Cs*DZ7eQopEJQ0Q}W1R3sZ#n&i%M#V= z>W%DIB>D!eUz0F#CHdi*LKjfHo`zd&M#a9#r_PDfYaa`7NS1Lw1mC`bnt z;Xv^;=0_r*PQJ)ZvXHJwpkO4jbaE0`g?@!8=SLz-=jR`IqGolAsewKAXo$Sj31P8J zJ>{1(EEMjj_oo}m5MPCH@bi{7q{Qtng;jFOZmOw;z6w=R1yZYoa6(0byhOzbA*_@_ zv2oeKvOz^xjWyO)#RgY}aK|{+sAsX_qwR^s>M5nBel|JavUlFO?0`-Bsp)_C0agRz UvTyrtv;Y7A07*qoM6N<$f-9N-CIA2c diff --git a/electron/indicator.test.cjs b/electron/indicator.test.cjs index 2369dada9c..46e91ac8e1 100644 --- a/electron/indicator.test.cjs +++ b/electron/indicator.test.cjs @@ -251,15 +251,50 @@ test('initIndicator uses NativeImage for Linux tray creation and updates', () => undefined, ); + // On Linux the running icon stays static (no progress-animation frames) to + // avoid StatusNotifierItem flicker (#4905). assert.equal(traySetImageCalls.at(-1).kind, 'native-image'); assert.match( traySetImageCalls.at(-1).iconPath, - /\/icons\/indicator\/running-anim-d\/3\.png$/, + /\/icons\/indicator\/running-d\.png$/, ); beforeQuitHandler(); }); +test('non-Linux platforms keep the running progress animation', () => { + Object.defineProperty(process, 'platform', { + configurable: true, + value: 'darwin', + }); + const { initIndicator } = loadIndicatorModule(); + + initIndicator({ + showApp: () => {}, + quitApp: () => {}, + ICONS_FOLDER: '/icons/', + forceDarkTray: false, + app: { on: () => {} }, + }); + + const currentTaskUpdated = ipcHandlers.get('CURRENT_TASK_UPDATED'); + currentTaskUpdated( + {}, + { id: 'T1', title: 'Task', timeSpent: 5 * 60000, timeEstimate: 25 * 60000 }, + false, + 0, + false, + 0, + undefined, + ); + + // macOS uses the black (`-l`) template icon and still animates progress. + assert.match( + traySetImageCalls.at(-1).iconPath, + /\/icons\/indicator\/running-anim-l\/3\.png$/, + ); +}); + test('initIndicator falls back to icon path if NativeImage creation is empty', () => { nextNativeImageIsEmpty = true; const { initIndicator } = loadIndicatorModule(); diff --git a/electron/indicator.ts b/electron/indicator.ts index 9ac2ce0016..dacef9b150 100644 --- a/electron/indicator.ts +++ b/electron/indicator.ts @@ -559,7 +559,12 @@ function createContextMenu(msg?: string): Menu { // eslint-disable-next-line prefer-arrow/prefer-arrow-functions function getRunningIconPath(progress?: number): string { const suf = shouldUseDarkColors ? '-d' : '-l'; - if (typeof progress === 'number' && progress > 0 && isFinite(progress)) { + // Linux StatusNotifierItem hosts redraw the whole tray item on every + // Tray.setImage(); cycling through the 16 progress-animation frames makes the + // icon visibly flicker between a full and a partial ring (#4905). Show a single + // static running icon there instead — elapsed/remaining time still shows in the + // tray context-menu label (and the title/OS taskbar where the host supports it). + if (!IS_LINUX && typeof progress === 'number' && progress > 0 && isFinite(progress)) { const f = Math.min(Math.round(progress * 15), 15); return DIR + `running-anim${suf}/${f || 0}.png`; }