From d016453f73553e6cc9ebc0d6f7beba4e7b2110f9 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Mon, 28 Sep 2026 19:47:21 +0200 Subject: [PATCH] feat(theseus): search-engine icons ship in the build; custom engines cache theirs on disk Every engine icon was an pointing at Google's favicon service, fetched again each time the picker, the toolbar or Settings rendered. Offline the whole list collapsed to the emoji fallbacks, and each open told Google which engines the user has configured. The catalog's icons now live in engine-icons/.png inside the app; a custom engine's icon is fetched once (its own /favicon.ico first, the favicon service as fallback), cached under the profile, and removed with the engine. Settings no longer falls through to DuckDuckGo's icon service either. Phind ships no icon: its site serves none through the bot wall. --- engine-icons/baidu.png | Bin 0 -> 2678 bytes engine-icons/bing.png | Bin 0 -> 548 bytes engine-icons/brave.png | Bin 0 -> 1125 bytes engine-icons/duckduckgo.png | Bin 0 -> 1477 bytes engine-icons/ecosia.png | Bin 0 -> 566 bytes engine-icons/google.png | Bin 0 -> 1063 bytes engine-icons/marginalia.png | Bin 0 -> 747 bytes engine-icons/metager.png | Bin 0 -> 631 bytes engine-icons/mojeek.png | Bin 0 -> 1821 bytes engine-icons/naver.png | Bin 0 -> 857 bytes engine-icons/perplexity.png | Bin 0 -> 805 bytes engine-icons/presearch.png | Bin 0 -> 365 bytes engine-icons/qwant.png | Bin 0 -> 2054 bytes engine-icons/startpage.png | Bin 0 -> 523 bytes engine-icons/stract.png | Bin 0 -> 648 bytes engine-icons/swisscows.png | Bin 0 -> 737 bytes engine-icons/wikipedia.png | Bin 0 -> 586 bytes engine-icons/yandex.png | Bin 0 -> 568 bytes engine-icons/yep.png | Bin 0 -> 659 bytes main.js | 106 +++++++++++++++++++++++++++++++----- package.json | 1 + settings.html | 18 ++---- 22 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 engine-icons/baidu.png create mode 100644 engine-icons/bing.png create mode 100644 engine-icons/brave.png create mode 100644 engine-icons/duckduckgo.png create mode 100644 engine-icons/ecosia.png create mode 100644 engine-icons/google.png create mode 100644 engine-icons/marginalia.png create mode 100644 engine-icons/metager.png create mode 100644 engine-icons/mojeek.png create mode 100644 engine-icons/naver.png create mode 100644 engine-icons/perplexity.png create mode 100644 engine-icons/presearch.png create mode 100644 engine-icons/qwant.png create mode 100644 engine-icons/startpage.png create mode 100644 engine-icons/stract.png create mode 100644 engine-icons/swisscows.png create mode 100644 engine-icons/wikipedia.png create mode 100644 engine-icons/yandex.png create mode 100644 engine-icons/yep.png diff --git a/engine-icons/baidu.png b/engine-icons/baidu.png new file mode 100644 index 0000000000000000000000000000000000000000..23bea1c540878d4c7f4950df024a72821324d694 GIT binary patch literal 2678 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 literal 0 HcmV?d00001 diff --git a/engine-icons/duckduckgo.png b/engine-icons/duckduckgo.png new file mode 100644 index 0000000000000000000000000000000000000000..7585458b1eaecd7fe36c4c8ee29fe57f30ca7712 GIT binary patch literal 1477 zcmV;$1v>hPP)LD{QKZvJN)kI?ya5fx~}4GOWtQd=ZA0k_4N1H z(CL_k?4XYO=;QLjw&{_3>Zq9Id|~*tHRVYzmj5GA5GXM74 zzv8<^T$`KFkejB*X~12uxYF*BGu@DV|K_0o*>8A?z500iwxL_t(o zh3!{qbK^D)B_lGtM3EwOI`SFExpv$9|6f;75@pH9ZaV$a&J;6oG7j+oJb(a**gq8X zKfrwep|o!^gfYt6B2hy8Edb@3j8aOt1VIG;G=1+8--M+bZ^-ru`*1b}d!^uh%sz~g2CU`WLhIv&tMbZjZ> z1q(D`nq*=Ipr~=ojuc+qSkZN=T_yrDL)8o*0Zw8BJo^e?b+3^G_WJVP74w?r2GUOT z44|N1IEiKe-OI~sJ^{RRBMGA)(gl?nKnuibbOG=Z0iIspSwcd4k|xr`Qqs-}4|m1hrj6nq=R!#7Kd7?xortR;!1HhoP#uXO%=V z01V9g2!PLBPyF}^O5x{W8$KRB#xQ;8zV`q~6ZwB0)dtya53wMx073|74{R*LA6r1! zt_C@LWK*N&#YG4i+^}@Y@=yp6K=6ILLwyPGd7tLZ1&JYz)wxh20C@1QH5dK;W*R*$ zSS%zULFbVJkd?t0taBWG#Y8y~RL%gJ!Z@2u;m?6Z!F~X90CD)(?LH2N)po1`E`_o* znE@yYovx1M&a=oOd;U33L(BrU%$dby5*g9w>x@Wuu7u|mSO$SK?BZ1b;}tRU>&%+a zP|#Ef`rH$9gUk*jGgB5I0%$130>DwZ24XKr7@|Xg!1;KA{#{D4Xlc;UfmAgWnt7UH z$j7f{MSw~eV0FF#Up*O)=!5QE{{1oRd^B1+$(l0l)rFfqu7h|Avq^*s z-ESTr_t61p=$>2+7N~8WOf!fF5{_@D)A8we+VA&|Ae)$Dn1IMkee*nAYkVHAyoJ&B z7S!JQ&lx~JT#3CNt}xy)WTqeQV5Z6kfX(A1q11U8@1EQm@4zFT7L1;R!S-hQkkf*} zZ!Q?HT%1=N{<&tS2(a1gHya3n#}!Aj+W;c%mmFuIY}zkz40cb))A3IzMqm>0`Zr4! z%fSa&2NleEqjLdfkKwG!LM>9GVa+RkwdR2jnv)i4wDcTEIG)Ceu0b88KwQ#doc?0b z3`|ObJRBA4*^W}mI;^^-#rQX?ZtxdYdy;;_cvZvu!dM?49EHtb@3YDG9vr>Dt}x-r1B<>Ho;+FOZmHjYbP1u7B0jpjR`=dq f|K@k&e+S?%)4heR?Zus&00000NkvXXu0mjfeBQ$E literal 0 HcmV?d00001 diff --git a/engine-icons/ecosia.png b/engine-icons/ecosia.png new file mode 100644 index 0000000000000000000000000000000000000000..ad65c189cc33061082b20f415320625ecab41b69 GIT binary patch literal 566 zcmV-60?GY}P)qbP9}beNo{yOsJLSe6951J00{C#+5i9n#7RU! zR9J=Wmdkp?AP9vc8ob2x{U5hU#>|We47QstdbSDwhah;CLpW zWB{4~ATNk)0$>yY8=3%eL!%8KXW%=4G+9awmaYc70N}e&Lr!76if{wI@Dh#ZPY|wr zA-!i1A4&v}x%h4slOoatxQn=Td8z>9Do9qT0ziJ0$R@i1m@d7?3s;EjVx^78iyOw( z<{b}cZKSP);TRryKzynHgEm3}ys57;eIup;LkdC-WB`35gf1uma+@~(9@vji)tkrw zhJj9VfN`MJVHyGBM(KS4)|IOJzB3yo_5`5n5@aW;$lioVvMN>rh;FN90|Jxgdt(O0 zBRNEF7xX$@`cc4c1+;|#d)lJSuKKmcSW0=sr*bORdDGu!T=4=Q)RvIlWm6T=Y#7zX z118hG<#70Rn6d&HF0^_5c6?07*qoM6N<$ Eg1^Y=?f?J) literal 0 HcmV?d00001 diff --git a/engine-icons/google.png b/engine-icons/google.png new file mode 100644 index 0000000000000000000000000000000000000000..256f76776b098af1d8c2cb3be2064df23b6a2f99 GIT binary patch literal 1063 zcmV+?1laqDP)`+5Ti1hrx z66;JqJgiztit|K<^(v=LI)n84{QJ}3{p&_J>OCyr@aXSrTb9qH`?>{|xA^$Q!T#^a zz~aeqzl8kd_fn7cEq(N6n)dalqW;{D{nBEtxk2)fh(4X2#@PGx zlq)==dOX?5UjP6A4s=pZQvjjsP}BS!Gq*pCW)gV;L^s0#00SdQL_t(oh2>XSo1!of z6cj`eP()*f!xOJjXLNS=|NpcN5|YjV;=?{|J^2Au>FVxuVyE-ZFtP`pVK|Oqcmw-w zw(UBm4`4d(J0jhluYuETNq0>h{TW9aiGgzCUCFY=YoNC6L^NZ}CxP6PT)l?TV&Q;@6<^;Ib!QVfbu<_|ZV)aFrT_ z1iwk)54eHG;m9f2t_p?KL6Pr7Qf^DJ&x_n;7nf>h??Pc;Q>tg^!5 zQ^_YTWQA)Pz+}N4U^deDwPc4Ps(_Y|p3?P&oxm?E;8@6M2qX`u9d(P-9R19d6vBgn zpR5)WAVEa=k{~}J6a;@}b14Jxdw+_HpA4v|=%5W{IHHKwQX;hw5AcFQMr%ElP#`1*@B)*W)S7w%!JBN5lEU%+jl zfZcjNsYUI-l?tXN|8Ll63crHw)JkBd-CsA`urwOjGTQ(18@Z0Cfi1^XFX+wadq&T+ h2+W@0y~^DGjlUxDv>wKie;5D&002ovPDHLkV1mN!4?_R| literal 0 HcmV?d00001 diff --git a/engine-icons/marginalia.png b/engine-icons/marginalia.png new file mode 100644 index 0000000000000000000000000000000000000000..09d2c46ba170ffbff1ddf1eef8f2faa2ac26c9f3 GIT binary patch literal 747 zcmV5Qe|%_RM$&BbjiOBnuV@6O>h?D9>{wioyoL4FVF^U;%Pig(!(~49*Y~VMzR# zo~|tX#$$Vs7i7UNl{A{}uKN40hdpw2-Qq4`?lk&6f&YO(DgU(xz&S@bj7Q$e*!nmW z=Tg!%C5qyl^?b}!V5M5sI0*)}R4Mf=%CWMpuCD6A!9n5Ra|r-kHJY@Vu4h@#T#lt& zUS6WAy0^DSl0=0FOi6*EE3+aNE`I;w{QMmCUFkcVXa-9Kpu{c zPX{J=R6IX^d;>t$v%US1{ryjRcJ>9(o=sv(3WkuLSwDW0H!q$Io)wGW+1S_wJiEKQ zgSB_>Kj`LWO@`iXNnj8}Ja3=BLQQ+>o_hU+N~MCTvbMHH9LHo?Hq&om0$!9(;P`xW zI-K%-TX;YJ_Apg}{$asdOB}~sH?C3ZdC&@oe1=hrGXi+56`boAn_^9bl7jD# z;pyXet^#qnT<9jCAU9e~AW4#$r)CmpwcFB{(ilsc=8ab0`_mz4POBSZ2C2z|+<%(g d6S!*v{{X1%G7`vnAl3i?002ovPDHLkV1go)S}*_r literal 0 HcmV?d00001 diff --git a/engine-icons/metager.png b/engine-icons/metager.png new file mode 100644 index 0000000000000000000000000000000000000000..06cef52249b2c0bf1daba433937cf5b0aae407d9 GIT binary patch literal 631 zcmV--0*L*IP)-|35I{qSC=AFhP^k+p{r^8Klz>Pk zfzUqmwBA?4H8YcCLdc6BgqjloV^sBhxl)oIj^|SdatCvbX_B35!*>LY!}ak=%oP|$ zK~ckefsy5yK|^2^SSob{hJ$3X6If>lWxLNF0t-n)Cy0E80$}F%-tZ<);UPea)Ih-t zxL{*05fEbbiL!+lW(kPD;)0kh6VQqWu@{oi2~sh-r(qFa3Wy+R*i@=5TW29{1v_CF z4a=I!7Giq_F0CLVqJh@c#`no^qE}u=u?+KI@qZ9dB`_(l*GlrKD%5M$QW8uESSdM# zM*Kzr&$546DhONRWH2fircEuNc9DGXH0qkyg}asaS%M8R%7Z2<=EsW#{BZEaAN2&L z7=)exwtrDDK2)^@<6~W0FuqtQ$@B!m)kfd-1j6MCABZ593xv%G*f#F^kp5Ed5gQD; zMg$Rorn@n3QuP32!_A)N4wTx256&6ScRTl2zaYo2jR^# zNSf_t4mq}(Mntl9zs-YJB&x4f4-P&F2v5%OkV>9BYsHi2F$)HZ9$hb%^9kiqABLw1 RQwIP5002ovPDHLkV1feSD%Jo1 literal 0 HcmV?d00001 diff --git a/engine-icons/mojeek.png b/engine-icons/mojeek.png new file mode 100644 index 0000000000000000000000000000000000000000..0c6cd78638c19509215201f2b13a3cdb0f18eaed GIT binary patch literal 1821 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 literal 0 HcmV?d00001 diff --git a/engine-icons/perplexity.png b/engine-icons/perplexity.png new file mode 100644 index 0000000000000000000000000000000000000000..f5901c2cb01735c859390186c80f27936c585936 GIT binary patch literal 805 zcmV+=1KRwFP)C>ycN(I{v-a}$|7i3df!!Q@%s0kixvVJSh@QGcyWJ)CJOLI zye@7WAYr%z4EQhsAYq)p0p^jOrOZHr+OB|2=|>4M*zrx!g@6tl$2}zmIF{DB`+!}? zrb`5f1przOXt7}t=>XB!0CjHx)_VY;Ed$Ic4Ok@{Tl)k+ongYYEEDjybby(-l=wT^ z0(l0|nTN(QbNK~Opp8aYQ_`4ga8K12K+QY4?g7BI*WUqcV51p7hV6HNb6&<~0^l;n zK6s$+0KU9o^&Gq4i;O7K0F(y{{&b70j7I>f9-E24B_a`k)gw1s3mfi3y*&Y-_?YTB zsu7b?p3)QtQ25N8Z8?*ixr~egXw9wpdn!^(f#{O}T$q_Al1!QXi~v0C=BnISQT_>- znW6>&(p3pl1VFE0dFLN2tC>Os@at$ZdXNF=A^`lIF0bV0IRY>Sx78?zVHhO`P)719 zz%Y*3=~hsTWSs!YU}}bHB<$Q1H{e+2QU?(ci~L#2Y};*H+itz@=6ELv}wnWM4FBQPX(M6Wizoe zvX}4VKz9F_)(4HWp>C@P^xN0P~@u&T!ccppHA)D_md~^;9Y; z#G(`66q^{pGnyC;mz#3C>jZei7!G6$Ohdm%mVu4fi-07MZ)8clM=yqg3pv0*I|t=3 zypDcZU>aEU_o+X_Y&j2@#*r-2aZ5?gdQ?0QVPEF6Km0{DG-5yK00000 LNkvXXu0mjf4;Pp8 literal 0 HcmV?d00001 diff --git a/engine-icons/qwant.png b/engine-icons/qwant.png new file mode 100644 index 0000000000000000000000000000000000000000..975f59805ee0bda51ee7b0c093dd093b6aeabc60 GIT binary patch literal 2054 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 literal 0 HcmV?d00001 diff --git a/engine-icons/startpage.png b/engine-icons/startpage.png new file mode 100644 index 0000000000000000000000000000000000000000..c9d557879893b577977c8e53fd9c7a113d5adc61 GIT binary patch literal 523 zcmV+m0`&cfP)-OXTr`%zhJm+R%&-SRg=Sw*qr&d}Dmyu{Av z_}=;WU!3uwxcfcF+F)pUCo@c&pQ+~P>X4)TtFO1Y&H8NR+<=CYOrq*?c!-LRn*aka zdxDWu1-vW(00CD?L_t(oh3%DHvw|QHhS6Z5L{l`sQcLUq|HfGIlwF-!nmIRz=c;Yy zeS}>^#sYyr;4g^dnER)N8Bdl}_&H*8s-ozy+wIPy@W_|?c)VVJ6e&GF)%C^@kqvrZ zqv7ZbRuK_*q4$=$Hk@&BI-uc9%9j%g^jaCW4bl<}=BHOVqJgvow}bLkiZeEXkT0F! zUGuT3s)!)qdeT~N{wR$}NrU9&=jn|RiU1Mswu>dPP{R3~WbN`UrBaZQ1PLf5i?!+# zB_#|Lfmm!rwS$OxEUe8Iqy;dR;zK1Q-O$Z|KilH>&<90^F$Lh_QfB1Wv1p&k6*ZGD zm9$at%mO$ARlr1k&u+JyYKjgG(2)&{yA#NnxPVU|H}nmTHOu`CIgV8|^t<(i8>^ao zf5Xt?M20WEQ;TgRTzoz;E204f9r;$+(BHGm7d>EjEWYSKAQ1QuJOJV+5z4i@wkiMs N002ovPDHLkV1m%V{;>c6 literal 0 HcmV?d00001 diff --git a/engine-icons/stract.png b/engine-icons/stract.png new file mode 100644 index 0000000000000000000000000000000000000000..282077fdba22e282d5b2cb22870ded058401b4a9 GIT binary patch literal 648 zcmV;30(bq1P)c~O7=<5Jx-Hl{ur)5-xL6{QCOJVOA7PyoMDh)S6q;~^Ah)SpQEZC1h)YxNA+~_} zZ2@B$#|GQ~IJ1%XBqU3NnD;clXWnlNlGuGlBwt9b`Tw%!R^x~}-AeqeNmVVmC5ax% zp8#I*H95Bc_@-3|_|+r12Jp?al9p_GBxzcO1k1DlDJKJ_IT06-Kz$|ltjO5Wa6US3bwAB@=_jQRGSmX2Ru zPe_svPtWWR#(a2s7SF9^1+H!;*>x1o;5x&-an~6l3Li0@FYx`xg^npR#P`5-zR0cv z-}@jw&vx9FPPvCx19ilSYrcTUK-*m=3TKjO_t3hH=2znohsvxeS`EZW+Q5XZ<(v>1 zaGl|Yx>q~RF>(1bL4vp+h?CXcHkzk+cy)oegj{D>DN?OQ-Lh(fwvKE%UmO+M#foTc zDeIU3^cv};C$3|pEONA=)29BI5gjHve=4Ekb(ImRiBEg1QnebcV~g^UXd7*^JyzYg zs^MviILtMP1A*^p4RV0yI9-Rb_#r`TAWqUxY2K^Ql17`ZLcZ>$2bwsIDnWHk6j#fr zfxMWyF{0Q*OVLt7f$!n(pXDxE-7Mn*>X`1qrvqu$=$B_$;l6%{ix zGj(-!%gf7VW@f;^z|+&yR8&-hgM+fNvNp|FW&i*H3UpFVQvmD#|F|IZtO-x1AvY)h z00K2hL_t(og}s;Qc7q@gg)P`LVc!Jd{U5ZLsC5BcIH&Vh?#FOv0BM@$`D(x5jreL` z&nCwA&mHwAe!k=U75YecHm@2zgs-M50SMdXmVbZoXzKJ3{tG~AQU^dtDj7Md7q%(v${|Ro(RBdSqu#{%IS7{ll6~}c zU{OFfMxI#0#{#Me=@H)*;utU{04#Id%iZTgKu@Oi1ax{)<{s_m2OPbXeV@*t^K%A^LtqnZZNVCVMh_Iq$)f`# z4|p%ege?L=(-^hVACAaN_e?j%11K)-4v zTezEZXX2_7+neAbfU;r&M*u@vp#oWenp~u$>|hQscLLjAB&PsFBk*{lyTnCt3J{wa z6B%L)qOya949w6C6m!fE5J^D){tb{qh}hO9HR1pO9hi@u)P;HsFd*?ebp;T>p?PHh z!lfO20^nUGpgIFv>wz^Vnz#hG5cPoSM^{@1XzL%I)lc8*$M^gB|G1-myr2KSA!a~> T<4t9k00000NkvXXu0mjfPnSD& literal 0 HcmV?d00001 diff --git a/engine-icons/wikipedia.png b/engine-icons/wikipedia.png new file mode 100644 index 0000000000000000000000000000000000000000..eb62ae3039794ef1b8570ebc605e5fd45edc3f4a GIT binary patch literal 586 zcmV-Q0=4~#P)zmzS4lXlTB^zO%Elot>T8+1Vo_Bao1ggoK2qrluSRPE!B? z0iQ`kK~z|U?Uc!4gdhxt*>OWr+-BUy_kYQPfSn%dU{5{pUHkgldF%(2BxkgRIPl6lS9q)gWR0y92H^NeHH+0KD!gA#5`0!PxpdmKedExDH~X zh%{C<1eb?U39+ZvNpf*K@Mhk@BGIah=-_Tw6x=@AgDgQ*S@|HWHP3u-dvp3B{(}Ou zm-Ce(sY701XWx0z2H2Zhklf0K7+-nrb~>m+90-J0mvfh5A~Kk{oxBXO2Vi4(a3Sb) z#>xkvd@7%)C8F+AoeoYG?-=ng2d0vhRlLBYcdKHZ1MPL{NV@CV*E5|7hSbnfo|P23 zE8LBS)$lx**yHvDVSK0sE>|W$;kt>Ba|L@LL?XKADC&*nrb=IJNCNi2zltr7r;+74X&Z(Nl~hVH2t(tlw_R@uc2i{Y#R$7P5-!2L(9A( zEbSEOz9M0B_)fi^zcxKmzP0brGT+X6q-fwAzLP;eMS7%)&^dgk`ThY<`ycEhkx1kw YU$_Jk>mv%;umAu607*qoM6N<$f}M{P)e6^|M&L#l7#qVPX6cQ_*Oppp_KS&Q}|dw{_*enj)D8F zqxzSN`kRjVfN=c1wfxAy_;q6a+t&Qg%KXE;_-t7I;@2V(kyt;4R)qfY5&H)$NU{dm4#+X%vy(m8oSAb!MnXd|HHq6$U% zQpt;44PU2An?Wc95Sno*?E!GGn#ia*1ECnzl57DWtpJRzZ@5{_OffGp_OJt(y9u)f zN=|AhwQi!)POtZoJpn2WmHjr)AS5a3pN?k$$fXlNS3f}@iSz4Bz~>RRn2>54vl{1a z4yYN3oj{&RM(VV6b70JTQZ5VtvF$M!H6Y9pH*U5yN*o>N#Y2;4zQJ+eF~76%IOLg{ zD;&K$Tc{8Mq4UgR99?`?&_7bhj*tv?|R}bS_%-=kx50e literal 0 HcmV?d00001 diff --git a/engine-icons/yep.png b/engine-icons/yep.png new file mode 100644 index 0000000000000000000000000000000000000000..4e9cf7da537156593b20509db613e0de2d22bbec GIT binary patch literal 659 zcmV;E0&M+>P)rbd>!G| zGEYkp+|V$zswcy_D0u~ho z#k2OA0005vNklbk9u>3e`^OoWAyww_Q2FZJ_m3| zIbXZOVy*X%vjAyphV#w_A(ndUGM@$Eg1Fg4h*obM2!&)XjPr$Y7;3$`(x%?o{m$1ee{20+5LdB6$QMJ1+pk)&ipP#^%9MMnjIy*AYVO>;8;>2n=V{GEA> z)fvB-Rwrv*eap})44jbgzCWLg>zQ>R1wxM`j#BPKzMG((of&F(t0E>&c0pHJCVit tO^J7ToFV=GCSLS)1nM_P{k5<8*e}EH5Uzn8qqP74002ovPDHLkV1k9ZC>a0% literal 0 HcmV?d00001 diff --git a/main.js b/main.js index 3543f9af..809310d8 100644 --- a/main.js +++ b/main.js @@ -4,7 +4,7 @@ // h, Sia s3, direct ip, redirect u). Tabs, nav controls, a search box, a home // page, and optional Tor onion routing. No system daemon; the app is the trust // boundary. -const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme, shell, dialog } = require("electron"); +const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme, shell, dialog, net } = require("electron"); const path = require("path"); const url = require("url"); const http = require("http"); @@ -162,16 +162,86 @@ const SEARCH_ENGINES = { // Engines enabled by default (shown in the toolbar dropdown). The rest are in the // catalog and can be turned on from Settings. Custom + detected engines are always on. const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing"]; -// DuckDuckGo's icon service reliably returns a favicon for ANY domain from one -// privacy-respecting host — far more robust than guessing /favicon.ico per site. -// Search-engine favicon source. DuckDuckGo's icons.duckduckgo.com/ip3/… -// service was returning inconsistent results (Brave, Bing, Yandex etc. -// came back 404 → the settings row fell through to an emoji). Google's -// /s2/favicons service is materially more reliable, returns a real 32×32 -// PNG for essentially every host, and doesn't require login. Kept as a -// single point so the fallback source can be swapped again in one place. -const faviconUrl = (domain) => (domain ? `https://www.google.com/s2/favicons?domain=${domain}&sz=32` : null); -function customFavicon(url) { try { return faviconUrl(new URL(String(url).replace("%s", "x")).hostname); } catch { return null; } } +// Search-engine icons. The catalog's icons ship inside the build +// (engine-icons/.png) so the toolbar, the picker and Settings look the +// same offline as online, and opening the picker no longer tells a favicon +// service which engines the user has configured — until 0.3.58 every icon +// was an pointing at Google's /s2/favicons, re-fetched on each open +// and falling through to the emoji whenever the network was away. A custom +// engine's icon is fetched once (the site's own /favicon.ico first, Google's +// service as the fallback) and cached under the profile; it shows the emoji +// until that lands. A catalog engine without a bundled file (Phind: its +// site is behind a bot wall and serves no icon) keeps the emoji too. +const ENGINE_ICON_DIR = path.join(__dirname, "engine-icons"); +const bundledEngineIcons = new Set((() => { try { return fs.readdirSync(ENGINE_ICON_DIR); } catch { return []; } })()); +const bundledIcon = (id) => (bundledEngineIcons.has(id + ".png") ? url.pathToFileURL(path.join(ENGINE_ICON_DIR, id + ".png")).href : null); +const customIconDir = () => path.join(app.getPath("userData"), "engine-icons"); +const ICON_EXTS = ["png", "ico", "jpg", "gif", "webp", "svg"]; +function engineHost(u) { try { return new URL(String(u).replace("%s", "x")).hostname.toLowerCase(); } catch { return ""; } } +function cachedIconFile(host) { + if (!host) return null; + for (const ext of ICON_EXTS) { const f = path.join(customIconDir(), `${host}.${ext}`); if (fs.existsSync(f)) return f; } + return null; +} +function customFavicon(u) { + const host = engineHost(u); + const f = cachedIconFile(host); + if (f) return url.pathToFileURL(f).href; + if (host) fetchEngineIcon(host).catch(() => {}); + return null; +} +// Drop a removed custom engine's cached icon unless another custom engine on +// the same host still uses it. +function dropCustomIcon(id) { + const eng = (settings.customEngines || []).find((e) => e.id === id); + const host = eng ? engineHost(eng.url) : ""; + if (!host || (settings.customEngines || []).some((e) => e.id !== id && engineHost(e.url) === host)) return; + const f = cachedIconFile(host); + if (f) try { fs.unlinkSync(f); } catch {} +} +const iconFetches = new Map(); // host → in-flight Promise, or the time the last attempt failed +const ICON_RETRY_MS = 60 * 60 * 1000; +function sniffImage(buf) { + if (buf.length < 8) return null; + if (buf.slice(1, 4).toString() === "PNG") return "png"; + if (buf.readUInt32BE(0) === 0x00000100) return "ico"; + if (buf[0] === 0xff && buf[1] === 0xd8) return "jpg"; + if (buf.slice(0, 4).toString() === "GIF8") return "gif"; + if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP") return "webp"; + if (/^\s*(<\?xml[^>]*>\s*)?(\s*)*]/i.test(buf.slice(0, 512).toString("utf8"))) return "svg"; + return null; +} +async function fetchIconBytes(u) { + const r = await net.fetch(u, { redirect: "follow", signal: AbortSignal.timeout(8000), headers: { accept: "image/*,*/*;q=0.5" } }); + if (!r.ok) throw new Error(`http ${r.status}`); + const buf = Buffer.from(await r.arrayBuffer()); + if (buf.length < 64 || buf.length > 512 * 1024) throw new Error(`size ${buf.length}`); + const ext = sniffImage(buf); + if (!ext) throw new Error("not an image"); + return { buf, ext }; +} +function fetchEngineIcon(host) { + if (!app.isReady()) return Promise.resolve(null); // net.fetch needs the app; the startup pass retries + const cur = iconFetches.get(host); + if (cur && typeof cur.then === "function") return cur; + if (typeof cur === "number" && Date.now() - cur < ICON_RETRY_MS) return Promise.resolve(null); + const p = (async () => { + let got = null; + for (const src of [`https://${host}/favicon.ico`, `https://www.google.com/s2/favicons?domain=${host}&sz=64`]) { + try { got = await fetchIconBytes(src); break; } catch {} + } + if (!got) { iconFetches.set(host, Date.now()); return null; } + const dir = customIconDir(); + fs.mkdirSync(dir, { recursive: true }); + const f = path.join(dir, `${host}.${got.ext}`); + fs.writeFileSync(f + ".tmp", got.buf); fs.renameSync(f + ".tmp", f); + iconFetches.delete(host); + emitEngines(); // the toolbar and the picker repaint with the real icon + return f; + })(); + iconFetches.set(host, p); + return p; +} function isEnabled(id) { return (settings.enabledEngines || DEFAULT_ENABLED).includes(id); } // Two-tier state: an engine is INSTALLED if it's in the user's Additional // list (visible in Settings), and ENABLED if it's currently toggled on @@ -183,7 +253,7 @@ function isInstalled(id) { } function allEngines() { const list = Object.entries(SEARCH_ENGINES).map(([id, e]) => - ({ id, name: e.name, sym: e.sym, favicon: faviconUrl(e.fav), kind: e.kind || "search", tier: e.tier || "catalog", builtin: true, installed: isInstalled(id), enabled: isEnabled(id) })); + ({ id, name: e.name, sym: e.sym, favicon: bundledIcon(id), kind: e.kind || "search", tier: e.tier || "catalog", builtin: true, installed: isInstalled(id), enabled: isEnabled(id) })); for (const c of settings.customEngines || []) list.push({ id: c.id, name: c.name, sym: c.sym || "🔍", favicon: customFavicon(c.url), kind: c.kind || "search", tier: "custom", builtin: false, installed: true, enabled: isEnabled(c.id) }); // Apply the user's custom order; ids not in engineOrder keep their natural order (stable sort). @@ -5626,7 +5696,7 @@ ipcMain.handle("set-search-engine", (_e, id) => { if (allEngines().some((e) => e.id === id)) { settings.searchEngine = id; saveSettings(); emitEngines(); } return settings.searchEngine; }); -ipcMain.handle("add-engine", (_e, eng) => { +ipcMain.handle("add-engine", async (_e, eng) => { // A custom engine needs a name and a URL template containing "%s". Adding // installs it in both the settings list AND the toolbar dropdown. if (eng && eng.name && eng.url && String(eng.url).includes("%s")) { @@ -5637,12 +5707,18 @@ ipcMain.handle("add-engine", (_e, eng) => { settings.enabledEngines = [...new Set([...(settings.enabledEngines || DEFAULT_ENABLED), id])]; settings.searchEngine = id; // select the one just added saveSettings(); emitEngines(); + // Settings pulls the list once on return, so give the icon fetch a moment + // to land; offline or slow, the row shows the emoji and the toolbar + // repaints on its own when the icon arrives. + const host = engineHost(eng.url); + if (host && !cachedIconFile(host)) await Promise.race([fetchEngineIcon(host).catch(() => null), new Promise((r) => setTimeout(r, 4000))]); } return { engines: allEngines(), current: settings.searchEngine }; }); ipcMain.handle("remove-engine", (_e, id) => { // Drop a custom engine entirely — from customEngines and any list that // referenced it. + dropCustomIcon(id); settings.customEngines = (settings.customEngines || []).filter((e) => e.id !== id); settings.enabledEngines = (settings.enabledEngines || DEFAULT_ENABLED).filter((x) => x !== id); if (settings.searchEngine === id) settings.searchEngine = enabledEnginesList()[0]?.id || "duckduckgo"; @@ -5654,6 +5730,7 @@ ipcMain.handle("remove-engine", (_e, id) => { // aliases to remove-engine (they don't live in installedEngines). ipcMain.handle("remove-from-list", (_e, id) => { if ((settings.customEngines || []).some((e) => e.id === id)) { + dropCustomIcon(id); settings.customEngines = (settings.customEngines || []).filter((e) => e.id !== id); } else if (SEARCH_ENGINES[id]) { settings.installedEngines = (settings.installedEngines || DEFAULT_ENABLED).filter((x) => x !== id); @@ -6532,6 +6609,9 @@ if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) { installRequestFilter(); migrateExtensionDirs(); initAddons(); + // Custom engines saved before icons were cached (or whose fetch never + // landed) get theirs once the startup burst is over. + setTimeout(() => { for (const c of settings.customEngines || []) customFavicon(c.url); }, 15000); webapps.init({ parentWindow: () => (win && !win.isDestroyed() ? win : undefined), ask: askSheet, diff --git a/package.json b/package.json index 5cd96395..f2595f09 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "webapps.js", "js-dialog.html", "js-dialog-preload.js", + "engine-icons/**/*", "lib/**/*", "package.json", "node_modules/**/*", diff --git a/settings.html b/settings.html index d11746ba..1d54d6b2 100644 --- a/settings.html +++ b/settings.html @@ -878,22 +878,14 @@ // search engines: default picker + custom-engine list + add form const sel = document.getElementById("searchEngine"); const esc = (s) => String(s || "").replace(/ { - let alt = ""; - try { - const u = new URL(e.favicon || "https://x"); - const domain = u.searchParams.get("domain") || u.hostname; - if (domain && domain !== "x") alt = `https://icons.duckduckgo.com/ip3/${domain}.ico`; - } catch {} const sym = (e.sym || "🔍").replace(/'/g, "'"); - const fallbackChain = alt - ? `this.onerror=null;this.src='${alt}';this.setAttribute('data-tried','1');this.onerror=function(){this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))};` - : `this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))`; return e.favicon - ? `` + ? `` : `${sym}`; }; const ENGINE_KINDS = [